Merge branch 'main' into Frame-Limited

This commit is contained in:
J.Teeuwissen 2023-03-30 20:38:05 +02:00
commit 9b410694fe
No known key found for this signature in database
GPG key ID: DB5F7A1ED8D478AD
591 changed files with 42667 additions and 28227 deletions

View file

@ -6,14 +6,21 @@ test-gen-llvm-wasm = "test -p roc_gen_wasm -p test_gen --no-default-features --f
[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"]
# 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"]
[target.'cfg(not(target = "wasm32-unknown-unknown"))']
# Sets the avx, avx2, sse2 and sse4.2 target-features correctly based on your CPU.
rustflags = ["-Ctarget-cpu=native"]
# 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.

View file

@ -1,4 +1,5 @@
on: [workflow_dispatch]
on:
workflow_dispatch:
# this cancels workflows currently in progress if you start a new one
concurrency:
@ -6,48 +7,59 @@ concurrency:
cancel-in-progress: true
jobs:
build-linux-x86_64-files:
fetch-releases:
runs-on: [ubuntu-20.04]
steps:
- uses: actions/checkout@v3
# note: moving this step to a bash script will not work, the GITHUB_TOKEN is not passed properly
- name: Fetch releases data and save to file. Authorization is used to prevent rate limiting.
env:
AUTH_HEADER: 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}'
run: |
curl --request GET \
--url https://api.github.com/repos/roc-lang/roc/releases \
--header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
--header 'content-type: application/json' \
--output roc_releases.json
./ci/get_releases_json.sh
# does a build with the surgical linker and also with the legacy linker
- run: ./ci/build_basic_cli.sh linux_x86_64 "--linker legacy"
- run: curl -OL $(./ci/get_latest_release_url.sh linuxTESTING_x86_64)
- run: curl -OL $(./ci/get_latest_release_url.sh macosTESTING_x86_64)
- run: curl -OL $(./ci/get_latest_release_url.sh macosTESTING_apple_silicon)
- name: Save .rh1, .rm2 and .o file
- 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 linuxTESTING_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-x86_64.rm2
basic-cli/src/linux-x86_64.rh1
basic-cli/src/metadata_linux-x86_64.rm
basic-cli/src/linux-x86_64.rh
basic-cli/src/linux-x86_64.o
build-macos-x86_64-files:
runs-on: [macos-11] # I expect the generated files to work on macOS 12
needs: [fetch-releases]
steps:
- uses: actions/checkout@v3
# note: moving this step to a bash script will not work, the GITHUB_TOKEN is not passed properly
- name: Fetch releases data and save to file. Authorization is used to prevent rate limiting due to shared IP of github's macos ci servers.
run: |
curl --request GET \
--url https://api.github.com/repos/roc-lang/roc/releases \
--header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
--header 'content-type: application/json' \
--output roc_releases.json
- name: Download the previously uploaded roc_nightly archives
uses: actions/download-artifact@v3
- run: ./ci/build_basic_cli.sh macos_x86_64
- run: ./ci/build_basic_cli.sh macosTESTING_x86_64
- name: Save .o files
uses: actions/upload-artifact@v3
@ -59,13 +71,14 @@ jobs:
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: fetch releases data and save to file
run: curl https://api.github.com/repos/roc-lang/roc/releases > roc_releases.json
- name: Download the previously uploaded roc_nightly archives
uses: actions/download-artifact@v3
- run: ./ci/build_basic_cli.sh silicon
- run: ./ci/build_basic_cli.sh macosTESTING_apple_silicon
- name: Save macos-arm64.o file
uses: actions/upload-artifact@v3
@ -84,43 +97,36 @@ jobs:
- name: remove all folders except the ci folder
run: ls | grep -v ci | xargs rm -rf
# note: moving this step to a bash script will not work, the GITHUB_TOKEN is not passed properly
- name: Fetch releases data and save to file. Authorization is used to prevent rate limiting.
run: |
curl --request GET \
--url https://api.github.com/repos/roc-lang/roc/releases \
--header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
--header 'content-type: application/json' \
--output roc_releases.json
- name: Download the previously uploaded files
uses: actions/download-artifact@v3
- run: echo "ROC_RELEASE_URL=$(./ci/get_latest_release_url.sh linux_x86_64)" >> $GITHUB_ENV
- name: Get the archive from the url.
run: curl -OL ${{ env.ROC_RELEASE_URL }}
- name: mv roc nightly and simplify name
run: mv $(ls -d artifact/* | grep "roc_nightly.*tar\.gz" | grep "linux") ./roc_nightly.tar.gz
- name: decompress the tar
run: ls | grep "roc_nightly.*tar\.gz" | xargs tar -xzvf
run: tar -xzvf roc_nightly.tar.gz
- name: delete tar
run: ls | grep -v "roc_nightly.*tar\.gz" | xargs rm -rf
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
- name: Download the previously uploaded files
uses: actions/download-artifact@v3
- run: cp macos-apple-silicon-files/* ./basic-cli/src
- run: cp linux-x86_64-files/* ./basic-cli/src
- run: cp macos-x86_64-files/* ./basic-cli/src
- run: ./roc_nightly/roc build --bundle=.tar.br ./basic-cli/src/main.roc
# change to tar.gz for fast test build
- run: |
echo "ARCHIVE_FORMAT='.tar.br'" >> "$GITHUB_ENV"
- run: echo "TAR_FILENAME=$(ls -d basic-cli/src/* | grep '.tar.br')" >> $GITHUB_ENV
- 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
@ -129,3 +135,43 @@ jobs:
path: |
${{ env.TAR_FILENAME }}
test-release-ubuntu:
needs: [create-brotli-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") ./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: cd basic-cli-platform && ls | grep "tar" | xargs tar -xzf
- name: prep testing http-get.roc
run: |
mv roc_nightly basic-cli-platform/.
cd basic-cli-platform
mkdir examples
cd examples
curl -OL https://raw.githubusercontent.com/roc-lang/basic-cli/main/examples/http-get.roc
sed -i 's/pf:\ \"[^"]*/pf:\ \"\.\.\/main.roc/g' http-get.roc
cd ..
curl -OL https://raw.githubusercontent.com/roc-lang/basic-cli/main/ci/expect_scripts/http-get.exp
- run: sudo apt install -y expect
- name: execute test
run: |
cd basic-cli-platform
expect http-get.exp

View file

@ -25,9 +25,5 @@ jobs:
# 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
# TODO build basic-cli for macos 11
#- name: test examples/helloWorld.roc separately because it is ignored by default
# run: cargo test --locked --release -p roc_cli cli_run::hello_world -- --ignored && sccache --show-stats
- name: test launching the editor
run: cargo test --release --locked editor_launch_test::launch -- --ignored # `--ignored` to run this test that is ignored for "normal" runs

View file

@ -16,9 +16,9 @@ jobs:
- name: create version.txt
run: ./ci/write_version.sh
- name: build release
run: RUSTFLAGS="-C target-cpu=x86-64" cargo build --features with_sound --release --locked
# target-cpu=x86-64 -> For maximal compatibility for all CPU's. Note that this setting will likely make the compiler slower.
- name: build release with lto
run: RUSTFLAGS="-C target-cpu=x86-64" cargo build --profile=release-with-lto --locked --features "editor" --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
@ -41,6 +41,10 @@ jobs:
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 }}

View file

@ -36,7 +36,11 @@ jobs:
run: ./ci/write_version.sh
- name: build nightly release
run: cargo build --locked --release
run: cargo build --locked --profile=release-with-lto --features "editor" --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 }}

View file

@ -22,7 +22,7 @@ jobs:
# this issue may be caused by using older versions of XCode
- name: build release
run: RUSTFLAGS="-C target-cpu=x86-64" cargo build --features with_sound --release --locked
run: RUSTFLAGS="-C target-cpu=x86-64" cargo build --profile=release-with-lto --locked --features "editor" --bin roc
# target-cpu=x86-64 -> For maximal compatibility for all CPU's. Note that this setting will likely make the compiler slower.
- name: get commit SHA
@ -36,6 +36,10 @@ jobs:
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 }}

View file

@ -19,13 +19,7 @@ jobs:
with:
clean: "true"
- uses: cachix/install-nix-action@v15
# to cache nix packages
- uses: cachix/cachix-action@v10
with:
name: enigmaticsunrise
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- uses: cachix/install-nix-action@v20
- 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

View file

@ -1,7 +1,5 @@
on:
workflow_dispatch:
schedule:
- cron: '0 13 * * *'
name: Test latest nightly release for macOS Apple Silicon
@ -35,16 +33,16 @@ jobs:
run: mv roc_nightly* roc_nightly
- name: test roc hello world
run: ./roc_nightly/roc examples/helloWorld.roc
run: cd roc_nightly && ./roc examples/helloWorld.roc
- name: test platform switching rust
run: ./roc_nightly/roc examples/platform-switching/rocLovesRust.roc
run: cd roc_nightly && ./roc examples/platform-switching/rocLovesRust.roc
- name: test platform switching zig
run: ./roc_nightly/roc examples/platform-switching/rocLovesZig.roc
run: cd roc_nightly && ./roc examples/platform-switching/rocLovesZig.roc
- name: test platform switching c
run: ./roc_nightly/roc examples/platform-switching/rocLovesC.roc
run: cd roc_nightly && ./roc examples/platform-switching/rocLovesC.roc

View file

@ -1,7 +1,5 @@
on:
workflow_dispatch:
schedule:
- cron: '0 13 * * *'
name: Test latest nightly release for macOS, ubu 20.04, ubu 22.04 x86_64
@ -52,10 +50,10 @@ jobs:
run: mv roc_nightly* roc_nightly
- name: test roc hello world
run: ./roc_nightly/roc examples/helloWorld.roc
run: cd roc_nightly && ./roc examples/helloWorld.roc
- name: test platform switching rust
run: ./roc_nightly/roc examples/platform-switching/rocLovesRust.roc
run: cd roc_nightly && ./roc examples/platform-switching/rocLovesRust.roc
- name: get OS to use for zig download
if: startsWith(matrix.os, 'ubuntu')
@ -73,10 +71,10 @@ jobs:
run: zig version
- name: test platform switching zig
run: ./roc_nightly/roc examples/platform-switching/rocLovesZig.roc
run: cd roc_nightly && ./roc examples/platform-switching/rocLovesZig.roc
- name: test platform switching c
run: ./roc_nightly/roc examples/platform-switching/rocLovesC.roc
run: cd roc_nightly && ./roc examples/platform-switching/rocLovesC.roc

View file

@ -36,12 +36,6 @@ jobs:
- name: regular rust tests
run: cargo test --locked --release && sccache --show-stats
- name: test hellow_world separately because it is ignored by default, the use of url platforms causes issues within nix
run: cargo test --locked --release -p roc_cli cli_run::hello_world -- --ignored && sccache --show-stats
- name: test parse_letter_counts separately because it is ignored by default, the use of url platforms causes issues within nix
run: cargo test --locked --release -p roc_cli cli_run::parse_letter_counts -- --ignored && 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

View file

@ -0,0 +1,44 @@
on: [pull_request]
name: windows - release build
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
RUST_BACKTRACE: 1
jobs:
windows-release-build:
name: windows-release-build
runs-on: windows-2022
env:
LLVM_SYS_130_PREFIX: C:\LLVM-13.0.1-win64
timeout-minutes: 150
steps:
- uses: actions/checkout@v2
- run: Add-Content -Path "$env:GITHUB_ENV" -Value "GITHUB_RUNNER_CPU=$((Get-CimInstance Win32_Processor).Name)"
- name: download and install zig
run: |
curl.exe --output "C:\zig-windows-x86_64-0.9.1.zip" --url https://ziglang.org/download/0.9.1/zig-windows-x86_64-0.9.1.zip
cd C:\
7z x zig-windows-x86_64-0.9.1.zip
Add-Content $env:GITHUB_PATH "C:\zig-windows-x86_64-0.9.1\"
- name: zig version
run: zig version
- name: install rust nightly 1.65
run: rustup install nightly-2022-09-17
- name: set up llvm 13
run: |
curl.exe -L -O -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://github.com/roc-lang/llvm-package-windows/releases/download/v13.0.1/LLVM-13.0.1-win64.7z
7z x LLVM-13.0.1-win64.7z -oC:\LLVM-13.0.1-win64
- name: cargo build release. Twice for zig lld-link error.
run: cargo build --locked --release || cargo build --locked --release

View file

@ -1,6 +1,6 @@
on: [pull_request]
name: Test windows build
name: windows - subset of tests
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@ -10,8 +10,8 @@ env:
RUST_BACKTRACE: 1
jobs:
windows-cargo-build:
name: windows-cargo-build
windows-test-subset:
name: windows-test-subset
runs-on: windows-2022
env:
LLVM_SYS_130_PREFIX: C:\LLVM-13.0.1-win64
@ -41,7 +41,7 @@ jobs:
- name: set up llvm 13
run: |
curl.exe -L -O https://github.com/roc-lang/llvm-package-windows/releases/download/v13.0.1/LLVM-13.0.1-win64.7z
curl.exe -L -O -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" https://github.com/roc-lang/llvm-package-windows/releases/download/v13.0.1/LLVM-13.0.1-win64.7z
7z x LLVM-13.0.1-win64.7z -oC:\LLVM-13.0.1-win64
- name: Build tests --release without running. Twice for zig lld-link error.

16
.gitignore vendored
View file

@ -5,8 +5,10 @@ zig-cache
.envrc
*.rs.bk
*.o
*.so
*.obj
*.dll
*.dylib
*.lib
*.def
*.tmp
@ -22,11 +24,10 @@ zig-cache
vgcore.*
# roc cache files
*.rh1
*.rm1
*.rh*
*.rm*
preprocessedhost
metadata
roc-cheaty-lib.so
#editors
.idea/
@ -72,4 +73,11 @@ roc_linux_x86_64.tar.gz
result
# tutorial
www/src/roc-tutorial
www/src/roc-tutorial
# 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

View file

@ -50,18 +50,32 @@ This command will generate the documentation in the [`generated-docs`](generated
- 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):
- If you don't have signing set up on your device and you only want to change a single file, it will be easier to use [github's edit button](https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files). This will sign your commit automatically.
- For multi-file or complex changes you will want to set up signing on your device:
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:
- If you don't have signing set up on your device and you only want to change a single file, it will be easier to use [github's edit button](https://docs.github.com/en/repositories/working-with-files/managing-files/editing-files). This will sign your commit automatically.
- For multi-file or complex changes you will want to set up signing on your device:
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:
```sh
git config --global commit.gpgsign true
```
#### 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.

1878
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,44 +1,40 @@
[workspace]
members = [
"crates/compiler/*",
"crates/vendor/*",
"crates/glue",
"crates/editor",
"crates/ast",
"crates/cli",
"crates/code_markup",
"crates/highlight",
"crates/error_macros",
"crates/reporting",
"crates/packaging",
"crates/repl_cli",
"crates/repl_eval",
"crates/repl_test",
"crates/repl_wasm",
"crates/repl_expect",
"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/compiler/*",
"crates/vendor/*",
"crates/glue",
"crates/editor",
"crates/ast",
"crates/cli",
"crates/cli_utils",
"crates/code_markup",
"crates/highlight",
"crates/error_macros",
"crates/reporting",
"crates/packaging",
"crates/repl_cli",
"crates/repl_eval",
"crates/repl_test",
"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",
]
exclude = [
"ci/benchmarks/bench-runner",
# Examples sometimes have Rust hosts in their platforms. The compiler should ignore those.
"crates/cli_testing_examples",
"examples",
# Ignore building these normally. They are only imported by tests.
# The tests will still correctly build them.
"crates/cli_utils",
"crates/compiler/test_mono_macros",
"crates/compiler/str",
# `cargo build` would cause roc_std to be built with default features which errors on windows
"crates/roc_std",
"ci/benchmarks/bench-runner",
# 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.
@ -47,6 +43,13 @@ 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.
#
@ -65,72 +68,121 @@ resolver = "2"
# 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/roc-lang/inkwell", branch = "master", features = [ "llvm13-0" ] }
inkwell = { git = "https://github.com/roc-lang/inkwell", branch = "inkwell-llvm-15", features = ["llvm13-0"] }
arrayvec = "0.7.2"
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"
bumpalo = { version = "3.11.1", features = ["collections"] }
capstone = "0.11.0"
clap = { version = "3.2.20", default-features = false, features = ["std", "color", "suggestions"] }
const_format = { version = "0.2.23", features = ["const_generics"] }
criterion = { git = "https://github.com/Anton-4/criterion.rs", features = ["html_reports"]}
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"
clap = { version = "3.2.23", default-features = false, features = ["std", "color", "suggestions"] }
colored = "2.0.0"
confy = { git = 'https://github.com/rust-cli/confy', features = ["yaml_conf"], default-features = false }
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"] }
criterion-perf-events = { git = "https://github.com/Anton-4/criterion-perf-events" }
crossbeam = "0.8.2"
dircpy = "0.3.14"
distance = "0.4.0"
encode_unicode = "1.0.0"
errno = "0.2.8"
errno = "0.3.0"
flate2 = "1.0.25"
fnv = "1.0.7"
fs_extra = "1.2.0"
hashbrown = { version = "0.12.3", features = [ "bumpalo" ] }
iced-x86 = { version = "1.15.0", default-features = false, features = ["std", "decoder", "op_code_info", "instr_info"] }
im = "15.0.0"
im-rc = "15.0.0"
indoc = "1.0.7"
insta = "1.20.0"
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.135"
libloading = "0.7.1"
libc = "0.2.139" # update roc_std/Cargo.toml on change
libfuzzer-sys = "0.4"
libloading = "0.7.4"
log = "0.4.17"
mach_object = "0.1"
maplit = "1.0.2"
memmap2 = "0.5.7"
mimalloc = { version = "0.1.26", default-features = false }
packed_struct = "0.10.0"
page_size = "0.4.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"
pretty_assertions = "1.3.0"
quickcheck = "1.0.3"
quickcheck_macros = "1.0.0"
regex = "1.5.5"
rustyline = {git = "https://github.com/roc-lang/rustyline", rev = "e74333c"}
rustyline-derive = {git = "https://github.com/roc-lang/rustyline", rev = "e74333c"}
serde = { version = "1.0.144", features = ["derive"] }
signal-hook = "0.3.14"
snafu = { version = "0.7.1", features = ["backtraces"] }
static_assertions = "1.1.0"
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.51"
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.14", 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" }
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"
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"] }
target-lexicon = "0.12.3"
tempfile = "3.2.0"
unicode-segmentation = "1.10.0"
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"
# 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.
object = { version = "0.29.0", features = ["read", "write"] }
# Optimizations based on https://deterministic.space/high-performance-rust.html
[profile.release]
lto = "thin"
codegen-units = 1
# debug = true # enable when profiling
[profile.bench]
lto = "thin"
codegen-units = 1
lto = "thin"
[profile.release-with-debug]
inherits = "release"
debug = true
inherits = "release"
[profile.release-with-lto]
lto = "thin" # TODO: We could consider full here since this is only used for packaged release on github.
inherits = "release"

2
FAQ.md
View file

@ -276,7 +276,7 @@ So why does Roc have the specific syntax changes it does? Here are some brief ex
- 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.)
- 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.) 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.
- 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.

View file

@ -8,16 +8,28 @@ Roc is not ready for a 0.1 release yet, but we do have:
- [frequently asked questions](https://github.com/roc-lang/roc/blob/main/FAQ.md)
- [Zulip chat](https://roc.zulipchat.com) for help, questions and discussions
If you'd like to get involved in contributing to the language, the Zulip chat is also the best place to get help with [good first issues](https://github.com/roc-lang/roc/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
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 [Zulip chat](https://roc.zulipchat.com), we're friendly!
## Sponsors
We are very grateful to our sponsors [NoRedInk](https://www.noredink.com/), [rwx](https://www.rwx.com), and [Tweede golf](https://tweedegolf.nl/en).
We are very grateful for our corporate sponsors [Vendr](https://www.vendr.com/), [rwx](https://www.rwx.com), and [Tweede golf](https://tweedegolf.nl/en).
[<img src="https://www.noredink.com/assets/logo-red-black-f6989d7567cf90b349409137595e99c52d036d755b4403d25528e0fd83a3b084.svg" height="60" alt="NoRedInk logo"/>](https://www.noredink.com/)
[<img src="https://user-images.githubusercontent.com/1094080/223597445-81755626-a080-4299-a38c-3c92e7548489.png" height="60" alt="Vendr logo"/>](https://www.vendr.com)
&nbsp;&nbsp;&nbsp;&nbsp;
[<img src="https://www.rwx.com/build/_assets/rwx_banner_transparent_cropped-RYV7W2KL.svg" height="60" alt="rwx logo"/>](https://www.rwx.com)
[<img src="https://www.rwx.com/rwx_banner.svg" height="60" alt="rwx logo"/>](https://www.rwx.com)
&nbsp;&nbsp;&nbsp;&nbsp;
[<img src="https://user-images.githubusercontent.com/1094080/183123052-856815b1-8cc9-410a-83b0-589f03613188.svg" height="60" alt="tweede golf logo"/>](https://tweedegolf.nl/en)
If you or your employer would like to sponsor Roc's development, please [DM Richard Feldman on Zulip](https://roc.zulipchat.com/#narrow/pm-with/281383-user281383)!
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)!
We'd also like to express our gratitude to each and every one of our fantastic [GitHub sponsors](https://github.com/sponsors/roc-lang/)! A special thanks to those sponsoring $25/month or more:
* [Christopher Dolan](https://github.com/cdolan)
* [Nick Gravgaard](https://github.com/nickgravgaard)
* [Aaron White](https://github.com/aaronwhite)
* [Zeljko Nesic](https://github.com/popara)
* [Shritesh Bhattarai](https://github.com/shritesh)
* [Richard Feldman](https://github.com/rtfeldman)
* [Ayaz Hafiz](https://github.com/ayazhafiz)
Thank you all so much for helping Roc progress!

View file

@ -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.0.1"
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.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[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.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
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"

View file

@ -7,7 +7,7 @@ edition = "2021"
[dependencies]
clap = { version = "3.1.15", features = ["derive"] }
regex = "1.5.5"
data-encoding = "2.3.2"
is_executable = "1.0.1"
regex = "1.5.5"
ring = "0.16.20"
data-encoding = "2.3.2"

View file

@ -5,17 +5,24 @@ set -euxo pipefail
git clone https://github.com/roc-lang/basic-cli.git
# Get the url of the latest release. We're not using the latest main source code for easier reproducibility.
RELEASE_URL=$(./ci/get_latest_release_url.sh $1)
cd basic-cli
git checkout new-release
cd ..
# get the archive from the url
curl -OL $RELEASE_URL
if [ "$(uname -m)" == "x86_64" ] && [ "$(uname -s)" == "Linux" ]; then
sudo apt-get install musl-tools
cd basic-cli/src # we cd to install the target for the right rust version
rustup target add x86_64-unknown-linux-musl
cd ../..
fi
mv $(ls -d artifact/* | grep "roc_nightly.*tar\.gz" | grep "$1") ./roc_nightly.tar.gz
# decompress the tar
ls | grep "roc_nightly.*tar\.gz" | xargs tar -xzvf
tar -xzvf roc_nightly.tar.gz
# delete tar
ls | grep -v "roc_nightly.*tar\.gz" | xargs rm -rf
rm roc_nightly.tar.gz
# simplify dir name
mv roc_nightly* roc_nightly
@ -23,14 +30,14 @@ mv roc_nightly* roc_nightly
cd roc_nightly
# build the basic cli platform
./roc build ../basic-cli/examples/file.roc
./roc build ../basic-cli/examples/countdown.roc
# 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/file.roc
then ./roc build $EXTRA_ARGS ../basic-cli/examples/countdown.roc
fi
cd ..

12
ci/get_releases_json.sh Executable file
View file

@ -0,0 +1,12 @@
#!/usr/bin/env bash
# retrieves roc_releases.json, expects AUTH_HEADER to be set
# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
set -euxo pipefail
curl --request GET \
--url https://api.github.com/repos/roc-lang/roc/releases \
--header '$AUTH_HEADER' \
--header 'content-type: application/json' \
--output roc_releases.json

View file

@ -3,7 +3,7 @@
# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/
set -euxo pipefail
cp target/release/roc ./roc # to be able to delete "target" later
cp target/release-with-lto/roc ./roc # to be able to delete "target" later
# delete unnecessary files and folders
git clean -fdx --exclude roc

View file

@ -60,7 +60,6 @@ The compiler includes the following sub-crates;
- `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.
- `roc_str` provides `Roc` styled collection [reference counting](https://en.wikipedia.org/wiki/Reference_counting). See [README.md](./compiler/str/README.md) for more information.
- `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.

View file

@ -1,39 +1,39 @@
[package]
name = "roc_ast"
version = "0.0.1"
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."
authors.workspace = true
edition.workspace = true
license.workspace = true
version.workspace = true
[dependencies]
roc_builtins = { path = "../compiler/builtins"}
roc_builtins = { path = "../compiler/builtins" }
roc_can = { path = "../compiler/can" }
roc_collections = { path = "../compiler/collections" }
roc_region = { path = "../compiler/region" }
roc_error_macros = { path = "../error_macros" }
roc_load = { path = "../compiler/load" }
roc_module = { path = "../compiler/module" }
roc_packaging = { path = "../packaging" }
roc_parse = { path = "../compiler/parse" }
roc_problem = { path = "../compiler/problem" }
roc_types = { path = "../compiler/types" }
roc_unify = { path = "../compiler/unify"}
roc_solve = { path = "../compiler/solve"}
roc_load = { path = "../compiler/load" }
roc_target = { path = "../compiler/roc_target" }
roc_error_macros = { path = "../error_macros" }
roc_packaging = { path = "../packaging" }
roc_region = { path = "../compiler/region" }
roc_reporting = { path = "../reporting" }
roc_solve = { path = "../compiler/solve" }
roc_target = { path = "../compiler/roc_target" }
roc_types = { path = "../compiler/types" }
roc_unify = { path = "../compiler/unify" }
ven_graph = { path = "../vendor/pathfinding" }
arrayvec.workspace = true
bumpalo.workspace = true
libc.workspace = true
page_size.workspace = true
snafu.workspace = true
libc.workspace = true
[dev-dependencies]
indoc.workspace = true
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["memoryapi"]}
winapi.workspace = true

View file

@ -9,7 +9,7 @@ use roc_module::{
use roc_region::all::Region;
use roc_types::{
subs::Variable,
types::{self, AnnotationSource, PReason, PatternCategory},
types::{self, AnnotationSource, IndexOrField, PReason, PatternCategory},
types::{Category, Reason},
};
@ -375,7 +375,7 @@ pub fn constrain_expr<'a>(
env.pool.add(ext_type),
);
let category = Category::RecordAccessor(field.as_str(env.pool).into());
let category = Category::Accessor(IndexOrField::Field(field.as_str(env.pool).into()));
let record_expected = Expected::NoExpectation(record_type.shallow_clone());
let record_con = Eq(

View file

@ -6,6 +6,7 @@ use roc_can::num::{
use roc_can::operator::desugar_expr;
use roc_collections::all::MutSet;
use roc_module::symbol::Symbol;
use roc_parse::ident::Accessor;
use roc_parse::{ast::Expr, pattern::PatternType};
use roc_problem::can::{Problem, RuntimeError};
use roc_region::all::{Loc, Region};
@ -295,7 +296,7 @@ pub fn expr_to_expr2<'a>(
)
}
RecordAccessorFunction(field) => (
AccessorFunction(Accessor::RecordField(field)) => (
Expr2::Accessor {
function_var: env.var_store.fresh(),
record_var: env.var_store.fresh(),

View file

@ -405,7 +405,7 @@ pub fn to_type2<'a>(
Type2::Variable(var)
}
Tuple { fields: _, ext: _ } => {
Tuple { elems: _, ext: _ } => {
todo!("tuple type");
}
Record { fields, ext, .. } => {

View file

@ -1,87 +1,81 @@
[package]
name = "roc_cli"
version = "0.0.1"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
repository = "https://github.com/roc-lang/roc"
edition = "2021"
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
bench = false
[features]
default = ["target-aarch64", "target-x86_64", "target-wasm32", "editor"]
default = ["target-aarch64", "target-x86_64", "target-wasm32"]
wasm32-cli-run = ["target-wasm32", "run-wasm32"]
i386-cli-run = ["target-x86"]
wasm32-cli-run = ["target-wasm32", "run-wasm32"]
editor = ["roc_editor"]
run-wasm32 = ["roc_wasm_interp"]
# Compiling for a different target than the current machine 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-arm = ["roc_build/target-arm", "roc_repl_cli/target-arm"]
target-wasm32 = ["roc_build/target-wasm32", "roc_repl_cli/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-wasm32 = ["roc_build/target-wasm32", "roc_repl_cli/target-wasm32"]
target-all = [
"target-aarch64",
"target-arm",
"target-x86",
"target-x86_64",
"target-wasm32"
]
target-all = ["target-aarch64", "target-arm", "target-x86", "target-x86_64", "target-wasm32"]
sanitizers = ["roc_build/sanitizers"]
[dependencies]
roc_collections = { path = "../compiler/collections" }
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_editor = { path = "../editor", optional = true }
roc_error_macros = { path = "../error_macros" }
roc_fmt = { path = "../compiler/fmt" }
roc_gen_llvm = { path = "../compiler/gen_llvm" }
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_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_packaging = { path = "../packaging" }
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 }
roc_reporting = { path = "../reporting" }
roc_target = { path = "../compiler/roc_target" }
roc_tracing = { path = "../tracing" }
roc_gen_llvm = {path = "../compiler/gen_llvm"}
roc_wasm_interp = { path = "../wasm_interp", optional = true }
ven_pretty = { path = "../vendor/pretty" }
indoc.workspace = true
bumpalo.workspace = true
clap.workspace = true
const_format.workspace = true
mimalloc.workspace = true
bumpalo.workspace = true
libc.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
strum.workspace = true
libloading.workspace = true
signal-hook.workspace = true
inkwell.workspace = true
# for now, uses unix/libc functions that windows does not support
[target.'cfg(not(windows))'.dependencies]
@ -89,15 +83,15 @@ roc_repl_expect = { path = "../repl_expect" }
[dev-dependencies]
pretty_assertions = "1.3.0"
roc_test_utils = { path = "../test_utils" }
roc_utils = { path = "../utils" }
indoc = "1.0.7"
serial_test = "0.9.0"
criterion = { git = "https://github.com/Anton-4/criterion.rs"}
cli_utils = { path = "../cli_utils" }
once_cell = "1.15.0"
parking_lot = "0.12"
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"

View file

@ -1,628 +0,0 @@
use bumpalo::Bump;
use roc_build::{
link::{
legacy_host_filename, link, preprocess_host_wasm32, preprocessed_host_filename,
rebuild_host, LinkType, LinkingStrategy,
},
program::{self, CodeGenBackend, CodeGenOptions},
};
use roc_builtins::bitcode;
use roc_load::{
EntryPoint, ExecutionMode, ExpectMetadata, LoadConfig, LoadMonomorphizedError, LoadedModule,
LoadingProblem, Threading,
};
use roc_mono::ir::OptLevel;
use roc_packaging::cache::RocCacheDir;
use roc_reporting::{
cli::Problems,
report::{RenderTarget, DEFAULT_PALETTE},
};
use roc_target::TargetInfo;
use std::{
path::Path,
time::{Duration, Instant},
};
use std::{path::PathBuf, thread::JoinHandle};
use target_lexicon::Triple;
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 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,
};
LoadConfig {
target_info,
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<u32>,
roc_cache_dir: RocCacheDir<'_>,
load_config: LoadConfig,
) -> Result<BuiltFile<'a>, 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,
)
}
#[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,
linking_strategy: LinkingStrategy,
prebuilt_requested: bool,
wasm_dev_stack_bytes: Option<u32>,
loaded: roc_load::MonomorphizedModule<'a>,
compilation_start: Instant,
) -> Result<BuiltFile<'a>, 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!(),
};
// 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 {
platform_main_roc.with_file_name(legacy_host_filename(target).unwrap())
}
} else {
platform_main_roc.with_file_name(preprocessed_host_filename(target).unwrap())
};
// For example, if we're loading the platform from a URL, it's automatically prebuilt
// even if the --prebuilt-platform=true CLI flag wasn't set.
let is_platform_prebuilt = prebuilt_requested || loaded.uses_prebuilt_platform;
let cwd = app_module_path.parent().unwrap();
let mut output_exe_path = cwd.join(&*loaded.output_path);
if let Some(extension) = operating_system.executable_file_ext() {
output_exe_path.set_extension(extension);
}
// 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 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| {
format!(
"{}_{}",
x.module_string(&loaded.interns),
x.as_str(&loaded.interns)
)
})
.collect();
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,
exposed_values,
exposed_closure_types,
);
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 = program::report_problems_monomorphized(&mut loaded);
let loaded = loaded;
enum HostRebuildTiming {
BeforeApp(u128),
ConcurrentWithApp(JoinHandle<u128>),
}
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 {} ms\n",
rebuild_duration
);
}
Some(HostRebuildTiming::BeforeApp(rebuild_duration))
} else {
Some(HostRebuildTiming::ConcurrentWithApp(rebuild_thread))
}
} else {
None
};
let (roc_app_bytes, code_gen_timing, expect_metadata) = program::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 Assembly from Mono IR",
code_gen_timing.code_gen,
);
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 {} ms\n",
rebuild_duration
);
}
}
// 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 app_o_file = tempfile::Builder::new()
.prefix("roc_app")
.suffix(&format!(".{}", operating_system.object_file_ext()))
.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 =
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, program::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 = match prebuilt_requested {
true => "Because I was run with --prebuilt-platform=true, ",
false => "",
};
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 with --prebuilt-platform=false
"#
),
prefix,
preprocessed_host_path.to_string_lossy(),
);
}
#[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,
exported_symbols: Vec<String>,
exported_closure_types: Vec<String>,
) -> std::thread::JoinHandle<u128> {
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 => {
roc_linker::build_and_preprocess_host(
opt_level,
&thread_local_target,
platform_main_roc.as_path(),
preprocessed_host_path.as_path(),
exported_symbols,
exported_closure_types,
);
// 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()
})
}
#[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: 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((
program::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<BuiltFile<'a>, BuildFileError<'a>> {
let triple = target_lexicon::Triple::host();
let code_gen_options = CodeGenOptions {
backend: CodeGenBackend::Llvm,
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,
)
}

View file

@ -3,13 +3,16 @@
#[macro_use]
extern crate const_format;
use build::BuiltFile;
use bumpalo::Bump;
use clap::{Arg, ArgMatches, Command, ValueSource};
use roc_build::link::{LinkType, LinkingStrategy};
use roc_build::program::{CodeGenBackend, CodeGenOptions};
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_load::{ExpectMetadata, LoadingProblem, Threading};
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;
@ -29,14 +32,9 @@ use target_lexicon::{
#[cfg(not(target_os = "linux"))]
use tempfile::TempDir;
pub mod build;
mod format;
pub use format::format;
use crate::build::{standard_load_config, BuildFileError, BuildOrdering};
const DEFAULT_ROC_FILENAME: &str = "main.roc";
pub const CMD_BUILD: &str = "build";
pub const CMD_RUN: &str = "run";
pub const CMD_DEV: &str = "dev";
@ -66,7 +64,8 @@ pub const FLAG_CHECK: &str = "check";
pub const FLAG_WASM_STACK_SIZE_KB: &str = "wasm-stack-size-kb";
pub const ROC_FILE: &str = "ROC_FILE";
pub const ROC_DIR: &str = "ROC_DIR";
pub const GLUE_FILE: &str = "GLUE_FILE";
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";
@ -281,17 +280,24 @@ pub fn build_app<'a>() -> Command<'a> {
.subcommand(Command::new(CMD_GLUE)
.about("Generate glue code between a platform's Roc API and its host language")
.arg(
Arg::new(ROC_FILE)
.help("The .roc file for the platform module")
Arg::new(GLUE_SPEC)
.help("The specification for how to translate Roc types into output files.")
.allow_invalid_utf8(true)
.required(true)
)
.arg(
Arg::new(GLUE_FILE)
.help("The filename for the generated glue code\n(Currently, this must be a .rs file because only Rust glue generation is supported so far.)")
Arg::new(GLUE_DIR)
.help("The directory for the generated glue code.\nNote: The implementation can write to any file in this directory.")
.allow_invalid_utf8(true)
.required(true)
)
.arg(
Arg::new(ROC_FILE)
.help("The .roc file whose exposed types should be translated.")
.allow_invalid_utf8(true)
.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")
@ -332,6 +338,7 @@ pub fn build_app<'a>() -> Command<'a> {
Arg::new(DIRECTORY_OR_FILES)
.multiple_values(true)
.required(false)
.allow_invalid_utf8(true)
.help("(optional) The directory or files to open on launch"),
),
)
@ -360,7 +367,6 @@ pub fn test(_matches: &ArgMatches, _triple: Triple) -> io::Result<i32> {
#[cfg(not(windows))]
pub fn test(matches: &ArgMatches, triple: Triple) -> io::Result<i32> {
use roc_build::program::report_problems_monomorphized;
use roc_gen_llvm::llvm::build::LlvmBackendMode;
use roc_load::{ExecutionMode, LoadConfig, LoadMonomorphizedError};
use roc_packaging::cache;
use roc_target::TargetInfo;
@ -520,7 +526,7 @@ pub fn build(
roc_cache_dir: RocCacheDir<'_>,
link_type: LinkType,
) -> io::Result<i32> {
use build::build_file;
use roc_build::program::build_file;
use BuildConfig::*;
let filename = matches.value_of_os(ROC_FILE).unwrap();
@ -594,15 +600,6 @@ pub fn build(
// so we don't want to spend time freeing these values
let arena = ManuallyDrop::new(Bump::new());
let code_gen_backend = if matches!(triple.architecture, Architecture::Wasm32) {
CodeGenBackend::Wasm
} else {
match matches.is_present(FLAG_DEV) {
true => CodeGenBackend::Assembly,
false => CodeGenBackend::Llvm,
}
};
let opt_level = if let BuildConfig::BuildAndRunIfNoErrors = config {
OptLevel::Development
} else {
@ -618,6 +615,25 @@ pub fn build(
}
}
};
let code_gen_backend = if matches!(triple.architecture, Architecture::Wasm32) {
CodeGenBackend::Wasm
} else {
match matches.is_present(FLAG_DEV) {
true => CodeGenBackend::Assembly,
false => {
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.is_present(FLAG_DEBUG);
let emit_timings = matches.is_present(FLAG_TIME);
@ -760,48 +776,6 @@ pub fn build(
}
}
fn handle_error_module(
mut module: roc_load::LoadedModule,
total_time: std::time::Duration,
filename: &OsStr,
print_run_anyway_hint: bool,
) -> io::Result<i32> {
debug_assert!(module.total_problems() > 0);
let problems = roc_build::program::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())
}
fn handle_loading_problem(problem: LoadingProblem) -> io::Result<i32> {
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)
}
}
}
fn roc_run<'a, I: IntoIterator<Item = &'a OsStr>>(
arena: &Bump,
opt_level: OptLevel,
@ -1323,40 +1297,3 @@ impl std::str::FromStr for Target {
}
}
}
// 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)
}
}

View file

@ -1,11 +1,11 @@
//! The `roc` binary that brings together all functionality in the Roc toolset.
use roc_build::link::LinkType;
use roc_cli::build::check_file;
use roc_build::program::check_file;
use roc_cli::{
build_app, format, test, BuildConfig, FormatMode, Target, CMD_BUILD, CMD_CHECK, CMD_DEV,
CMD_DOCS, CMD_EDIT, CMD_FORMAT, CMD_GEN_STUB_LIB, CMD_GLUE, CMD_REPL, CMD_RUN, CMD_TEST,
CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_LIB, FLAG_NO_LINK, FLAG_TARGET, FLAG_TIME,
GLUE_FILE, ROC_FILE,
GLUE_DIR, GLUE_SPEC, ROC_FILE,
};
use roc_docs::generate_docs_html;
use roc_error_macros::user_error;
@ -88,12 +88,13 @@ fn main() -> io::Result<()> {
}
Some((CMD_GLUE, matches)) => {
let input_path = Path::new(matches.value_of_os(ROC_FILE).unwrap());
let output_path = Path::new(matches.value_of_os(GLUE_FILE).unwrap());
let output_path = Path::new(matches.value_of_os(GLUE_DIR).unwrap());
let spec_path = Path::new(matches.value_of_os(GLUE_SPEC).unwrap());
if Some("rs") == output_path.extension().and_then(OsStr::to_str) {
roc_glue::generate(input_path, output_path)
if !output_path.exists() || output_path.is_dir() {
roc_glue::generate(input_path, output_path, spec_path)
} else {
eprintln!("Currently, `roc glue` only supports generating Rust glue files (with the .rs extension). In the future, the plan is to decouple `roc glue` from any particular output format, by having it accept a second .roc file which gets executed as a plugin to generate glue code for any desired language. However, this has not yet been implemented, and for now only .rs is supported.");
eprintln!("`roc glue` must be given a directory to output into, because the glue might generate multiple files.");
Ok(1)
}

View file

@ -474,7 +474,8 @@ mod cli_run {
}
#[test]
#[ignore = "Prebuilt platforms cause problems with nix and NixOS. This is run explicitly tested on CI (.github/workflows/ubuntu_x86_64.yml)"]
#[serial(basic_cli_url)]
#[cfg_attr(windows, ignore)]
fn hello_world() {
test_roc_app_slim(
"examples",
@ -533,6 +534,7 @@ mod cli_run {
}
#[test]
#[serial(zig_platform)]
#[cfg_attr(windows, ignore)]
fn platform_switching_zig() {
test_roc_app_slim(
@ -675,6 +677,8 @@ mod cli_run {
)
}
#[cfg_attr(windows, ignore)] // flaky error; issue #5024
#[serial(breakout)]
#[test]
fn breakout() {
test_roc_app_slim(
@ -686,6 +690,18 @@ mod cli_run {
)
}
#[test]
#[serial(breakout)]
fn breakout_hello_gui() {
test_roc_app_slim(
"examples/gui/breakout",
"hello-gui.roc",
"hello-gui",
"",
UseValgrind::No,
)
}
#[test]
#[cfg_attr(windows, ignore)]
fn quicksort() {
@ -790,10 +806,10 @@ mod cli_run {
"False.roc",
"false",
&[],
&[Arg::ExamplePath("examples/hello.false")],
&[Arg::ExamplePath("examples/sqrt.false")],
&[],
&("Hello, World!".to_string() + LINE_ENDING),
UseValgrind::No,
"1414",
UseValgrind::Yes,
TestCliCommands::Many,
)
}
@ -819,7 +835,7 @@ mod cli_run {
&[],
&[Arg::ExamplePath("input"), Arg::ExamplePath("output")],
&[],
"Processed 3 files with 3 successes and 0 errors\n",
"Processed 4 files with 3 successes and 0 errors\n",
UseValgrind::No,
TestCliCommands::Run,
)
@ -849,6 +865,8 @@ mod cli_run {
}
#[test]
#[serial(parser_package)]
#[serial(zig_platform)]
#[cfg_attr(windows, ignore)]
fn parse_movies_csv() {
test_roc_app_slim(
@ -861,7 +879,9 @@ mod cli_run {
}
#[test]
#[ignore = "Prebuilt platforms cause problems with nix and NixOS. This is run explicitly tested on CI (.github/workflows/ubuntu_x86_64.yml)"]
#[serial(parser_package)]
#[serial(basic_cli_url)]
#[cfg_attr(windows, ignore)]
fn parse_letter_counts() {
test_roc_app_slim(
"examples/parser/examples",

View file

@ -8,25 +8,40 @@ mod editor_launch_test {
thread,
};
use cli_utils::helpers::build_roc_bin_cached;
use cli_utils::helpers::build_roc_bin;
use roc_cli::CMD_EDIT;
use roc_utils::root_dir;
use roc_command_utils::root_dir;
use std::io::Read;
// ignored because we don't want to bring up the editor window during regular tests, only on specific CI machines
#[ignore]
#[ignore = "We don't want to bring up the editor window during regular tests, only on specific CI machines."]
#[test]
fn launch() {
fn launch_test() {
launch(None);
// with a file arg
launch(Some("roc-projects/new-roc-project-1/main.roc"));
// with a folder arg
launch(Some("roc-projects/new-roc-project-1"));
}
fn launch(arg_path_str_opt: Option<&str>) {
let root_dir = root_dir();
// The editor expects to be run from the root of the repo, so it can find the cli-platform to init a new project folder.
env::set_current_dir(&root_dir)
.unwrap_or_else(|_| panic!("Failed to set current dir to {:?}", root_dir));
let roc_binary_path = build_roc_bin_cached();
let roc_binary_path = build_roc_bin(&["--features", "editor"]);
let mut cmd_args = vec![CMD_EDIT];
if let Some(arg_path_str) = arg_path_str_opt {
cmd_args.push(arg_path_str)
}
let mut roc_process = Command::new(roc_binary_path)
.arg(CMD_EDIT)
.args(cmd_args)
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start editor from cli.");

View file

@ -115,7 +115,7 @@ pub export fn main() i32 {
// stdout the result
stdout.print("{s}\n", .{callresult.asSlice()}) catch unreachable;
callresult.deinit();
callresult.decref();
stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable;

View file

@ -114,7 +114,7 @@ pub export fn main() i32 {
// stdout the result
stdout.print("{s}\n", .{callresult.asSlice()}) catch unreachable;
callresult.deinit();
callresult.decref();
stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable;

View file

@ -115,7 +115,7 @@ pub export fn main() i32 {
// stdout the result
stdout.print("{s}\n", .{callresult.asSlice()}) catch unreachable;
callresult.deinit();
callresult.decref();
stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable;

View file

@ -25,10 +25,10 @@ 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;
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;
@ -123,7 +123,7 @@ 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 = std.math.max(@intCast(usize, roc__mainForHost_size()), 8);
const size = std.math.max(@intCast(usize, roc__mainForHost_1_exposed_size()), 8);
const raw_output = roc_alloc(@intCast(usize, size), @alignOf(u64)).?;
var output = @ptrCast([*]u8, raw_output);
@ -155,7 +155,7 @@ 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 = std.math.max(roc__mainForHost_1__Fx_result_size(), 8);
const size = std.math.max(roc__mainForHost_0_result_size(), 8);
const raw_output = allocator.allocAdvanced(u8, @alignOf(u64), @intCast(usize, size), .at_least) catch unreachable;
var output = @ptrCast([*]u8, raw_output);
@ -165,7 +165,7 @@ fn call_the_closure(closure_data_pointer: [*]u8) void {
const flags: u8 = 0;
roc__mainForHost_1__Fx_caller(&flags, closure_data_pointer, output);
roc__mainForHost_0_caller(&flags, closure_data_pointer, output);
// The closure returns result, nothing interesting to do with it
return;

View file

@ -134,7 +134,7 @@ pub fn main() u8 {
// stdout the result
stdout.print("{s}", .{callresult.asSlice()}) catch unreachable;
callresult.deinit();
callresult.decref();
stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable;

View file

@ -1,27 +1,25 @@
[package]
name = "cli_utils"
version = "0.0.1"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
repository = "https://github.com/roc-lang/roc"
edition = "2021"
description = "Provides shared code for cli tests and benchmarks."
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
version.workspace = true
[dependencies]
roc_collections = { path = "../compiler/collections" }
roc_reporting = { path = "../reporting" }
roc_load = { path = "../compiler/load" }
roc_module = { path = "../compiler/module" }
roc_utils = { path = "../utils" }
roc_reporting = { path = "../reporting" }
roc_command_utils = { path = "../utils/command" }
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"
bumpalo.workspace = true
criterion.workspace = true
serde-xml-rs.workspace = true
serde.workspace = true
tempfile.workspace = true
[target.'cfg(unix)'.dependencies]
rlimit = "0.6.2"
rlimit.workspace = true

View file

@ -4,9 +4,7 @@ extern crate roc_load;
extern crate roc_module;
extern crate tempfile;
use roc_utils::cargo;
use roc_utils::pretty_command_string;
use roc_utils::root_dir;
use roc_command_utils::{cargo, pretty_command_string, root_dir};
use serde::Deserialize;
use serde_xml_rs::from_str;
use std::env;
@ -16,6 +14,7 @@ use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::process::{Command, ExitStatus, Stdio};
use std::sync::Mutex;
use tempfile::NamedTempFile;
#[derive(Debug)]
@ -41,49 +40,67 @@ pub fn build_roc_bin_cached() -> PathBuf {
let roc_binary_path = path_to_roc_binary();
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 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()
);
}
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
}
// 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 libary once and then caching it.
static GLUE_LOCK: Mutex<()> = Mutex::new(());
pub fn run_glue<I, S>(args: I) -> Out
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let _guard = GLUE_LOCK.lock().unwrap();
run_roc_with_stdin(&path_to_roc_binary(), args, &[])
}
@ -387,7 +404,7 @@ pub fn cli_testing_dir(dir_name: &str) -> PathBuf {
// Descend into examples/{dir_name}
path.push("crates");
path.push("cli_testing_examples");
path.extend(dir_name.split("/")); // Make slashes cross-target
path.extend(dir_name.split('/')); // Make slashes cross-target
path
}
@ -396,7 +413,7 @@ pub fn cli_testing_dir(dir_name: &str) -> PathBuf {
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.extend(dir_name.split('/')); // Make slashes cross-target
path
}
@ -419,7 +436,7 @@ pub fn fixtures_dir(dir_name: &str) -> PathBuf {
path.push("cli");
path.push("tests");
path.push("fixtures");
path.extend(dir_name.split("/")); // Make slashes cross-target
path.extend(dir_name.split('/')); // Make slashes cross-target
path
}

View file

@ -1,16 +1,19 @@
[package]
name = "roc_code_markup"
version = "0.0.1"
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."
authors.workspace = true
edition.workspace = true
license.workspace = true
version.workspace = true
[dependencies]
roc_ast = { path = "../ast" }
roc_module = { path = "../compiler/module" }
roc_utils = { path = "../utils" }
serde = { version = "1.0.144", features = ["derive"] }
palette = "0.6.1"
snafu = { version = "0.7.1", features = ["backtraces"] }
bumpalo = { version = "3.11.1", features = ["collections"] }
roc_error_utils = { path = "../utils/error" }
palette.workspace = true
bumpalo.workspace = true
serde.workspace = true
snafu.workspace = true

View file

@ -16,7 +16,7 @@ use roc_ast::{
lang::{core::ast::ASTNodeId, env::Env},
mem_pool::pool_str::PoolStr,
};
use roc_utils::{index_of, slice_get};
use roc_error_utils::{index_of, slice_get};
use std::fmt;
use std::fmt::Write;

View file

@ -1,4 +1,4 @@
use roc_utils::util_error::UtilError;
use roc_error_utils::UtilError;
use snafu::{Backtrace, NoneError, ResultExt, Snafu};
use crate::slow_pool::MarkNodeId;

View file

@ -1,14 +1,17 @@
[package]
authors = ["The Roc Contributors"]
edition = "2021"
license = "UPL-1.0"
name = "roc_alias_analysis"
version = "0.0.1"
authors.workspace = true
edition.workspace = true
license.workspace = true
version.workspace = true
[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"}
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

View file

@ -9,6 +9,7 @@ use morphic_lib::{
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;
@ -187,20 +188,28 @@ where
let func_name = FuncName(&bytes);
if let HostExposedLayouts::HostExposed { aliases, .. } = &proc.host_exposed_layouts {
for (_, (symbol, top_level, layout)) in aliases {
match layout {
for (_, hels) in aliases {
match hels.raw_function_layout {
RawFunctionLayout::Function(_, _, _) => {
let it = top_level.arguments.iter().copied();
let bytes =
func_name_bytes_help(*symbol, it, Niche::NONE, top_level.result);
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, top_level.arguments));
host_exposed_functions.push((bytes, hels.proc_layout.arguments));
}
RawFunctionLayout::ZeroArgumentThunk(_) => {
let bytes =
func_name_bytes_help(*symbol, [], Niche::NONE, top_level.result);
let bytes = func_name_bytes_help(
hels.symbol,
[],
Niche::NONE,
hels.proc_layout.result,
);
host_exposed_functions.push((bytes, top_level.arguments));
host_exposed_functions.push((bytes, hels.proc_layout.arguments));
}
}
}
@ -365,13 +374,7 @@ fn build_entry_point<'a>(
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,
&WhenRecursive::Unreachable,
)?;
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)?;
@ -401,13 +404,7 @@ fn build_entry_point<'a>(
let block = builder.add_block();
let struct_layout = interner.insert(Layout::struct_no_name_order(layouts));
let type_id = layout_spec(
env,
&mut builder,
interner,
struct_layout,
&WhenRecursive::Unreachable,
)?;
let type_id = layout_spec(env, &mut builder, interner, struct_layout)?;
let argument = builder.add_unknown_with(block, &[], type_id)?;
@ -466,20 +463,8 @@ fn proc_spec<'a>(
let args_struct_layout = interner.insert(Layout::struct_no_name_order(
argument_layouts.into_bump_slice(),
));
let arg_type_id = layout_spec(
&mut env,
&mut builder,
interner,
args_struct_layout,
&WhenRecursive::Unreachable,
)?;
let ret_type_id = layout_spec(
&mut env,
&mut builder,
interner,
proc.ret_layout,
&WhenRecursive::Unreachable,
)?;
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, proc.ret_layout)?;
let spec = builder.build(arg_type_id, ret_type_id, root)?;
@ -617,17 +602,10 @@ fn stmt_spec<'a>(
let mut type_ids = Vec::new();
for p in parameters.iter() {
type_ids.push(layout_spec(
env,
builder,
interner,
p.layout,
&WhenRecursive::Unreachable,
)?);
type_ids.push(layout_spec(env, builder, interner, p.layout)?);
}
let ret_type_id =
layout_spec(env, builder, interner, layout, &WhenRecursive::Unreachable)?;
let ret_type_id = layout_spec(env, builder, interner, layout)?;
let jp_arg_type_id = builder.add_tuple_type(&type_ids)?;
@ -668,8 +646,7 @@ fn stmt_spec<'a>(
builder.add_sub_block(block, BlockExpr(cont_block, cont_value_id))
}
Jump(id, symbols) => {
let ret_type_id =
layout_spec(env, builder, interner, layout, &WhenRecursive::Unreachable)?;
let ret_type_id = layout_spec(env, builder, interner, layout)?;
let argument = build_tuple_value(builder, env, block, symbols)?;
let jpid = env.join_points[id];
@ -678,8 +655,7 @@ fn stmt_spec<'a>(
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, layout, &WhenRecursive::Unreachable)?;
let result_type = layout_spec(env, builder, interner, layout)?;
builder.add_unknown_with(block, &[env.symbols[msg]], result_type)
}
@ -708,23 +684,16 @@ fn build_tuple_value(
builder.add_make_tuple(block, &value_ids)
}
#[derive(Clone, Debug, PartialEq)]
enum WhenRecursive<'a> {
Unreachable,
Loop(UnionLayout<'a>),
}
fn build_recursive_tuple_type<'a>(
env: &mut Env<'a>,
builder: &mut impl TypeContext,
interner: &STLayoutInterner<'a>,
layouts: &[InLayout<'a>],
when_recursive: &WhenRecursive,
) -> Result<TypeId> {
let mut field_types = Vec::new();
for field in layouts.iter() {
let type_id = layout_spec_help(env, builder, interner, *field, when_recursive)?;
let type_id = layout_spec_help(env, builder, interner, *field)?;
field_types.push(type_id);
}
@ -736,12 +705,11 @@ fn build_tuple_type<'a>(
builder: &mut impl TypeContext,
interner: &STLayoutInterner<'a>,
layouts: &[InLayout<'a>],
when_recursive: &WhenRecursive,
) -> Result<TypeId> {
let mut field_types = Vec::new();
for field in layouts.iter() {
field_types.push(layout_spec(env, builder, interner, *field, when_recursive)?);
field_types.push(layout_spec(env, builder, interner, *field)?);
}
builder.add_tuple_type(&field_types)
@ -811,13 +779,7 @@ fn call_spec<'a>(
.map(|symbol| env.symbols[symbol])
.collect();
let result_type = layout_spec(
env,
builder,
interner,
*ret_layout,
&WhenRecursive::Unreachable,
)?;
let result_type = layout_spec(env, builder, interner, *ret_layout)?;
builder.add_unknown_with(block, &arguments, result_type)
}
@ -888,23 +850,11 @@ fn call_spec<'a>(
list_append(builder, block, update_mode_var, state, new_element)
};
let output_element_type = layout_spec(
env,
builder,
interner,
*return_layout,
&WhenRecursive::Unreachable,
)?;
let output_element_type = layout_spec(env, builder, interner, *return_layout)?;
let state_layout =
interner.insert(Layout::Builtin(Builtin::List(*return_layout)));
let state_type = layout_spec(
env,
builder,
interner,
state_layout,
&WhenRecursive::Unreachable,
)?;
let state_type = layout_spec(env, builder, interner, state_layout)?;
let init_state = new_list(builder, block, output_element_type)?;
@ -931,13 +881,7 @@ fn call_spec<'a>(
let arg0_layout = argument_layouts[0];
let state_layout = interner.insert(Layout::Builtin(Builtin::List(arg0_layout)));
let state_type = layout_spec(
env,
builder,
interner,
state_layout,
&WhenRecursive::Unreachable,
)?;
let state_type = layout_spec(env, builder, interner, state_layout)?;
let init_state = list;
add_loop(builder, block, state_type, init_state, loop_body)
@ -961,23 +905,11 @@ fn call_spec<'a>(
list_append(builder, block, update_mode_var, state, new_element)
};
let output_element_type = layout_spec(
env,
builder,
interner,
*return_layout,
&WhenRecursive::Unreachable,
)?;
let output_element_type = layout_spec(env, builder, interner, *return_layout)?;
let state_layout =
interner.insert(Layout::Builtin(Builtin::List(*return_layout)));
let state_type = layout_spec(
env,
builder,
interner,
state_layout,
&WhenRecursive::Unreachable,
)?;
let state_type = layout_spec(env, builder, interner, state_layout)?;
let init_state = new_list(builder, block, output_element_type)?;
@ -1007,23 +939,11 @@ fn call_spec<'a>(
list_append(builder, block, update_mode_var, state, new_element)
};
let output_element_type = layout_spec(
env,
builder,
interner,
*return_layout,
&WhenRecursive::Unreachable,
)?;
let output_element_type = layout_spec(env, builder, interner, *return_layout)?;
let state_layout =
interner.insert(Layout::Builtin(Builtin::List(*return_layout)));
let state_type = layout_spec(
env,
builder,
interner,
state_layout,
&WhenRecursive::Unreachable,
)?;
let state_type = layout_spec(env, builder, interner, state_layout)?;
let init_state = new_list(builder, block, output_element_type)?;
@ -1059,23 +979,11 @@ fn call_spec<'a>(
list_append(builder, block, update_mode_var, state, new_element)
};
let output_element_type = layout_spec(
env,
builder,
interner,
*return_layout,
&WhenRecursive::Unreachable,
)?;
let output_element_type = layout_spec(env, builder, interner, *return_layout)?;
let state_layout =
interner.insert(Layout::Builtin(Builtin::List(*return_layout)));
let state_type = layout_spec(
env,
builder,
interner,
state_layout,
&WhenRecursive::Unreachable,
)?;
let state_type = layout_spec(env, builder, interner, state_layout)?;
let init_state = new_list(builder, block, output_element_type)?;
@ -1130,7 +1038,7 @@ fn lowlevel_spec<'a>(
) -> Result<ValueId> {
use LowLevel::*;
let type_id = layout_spec(env, builder, interner, layout, &WhenRecursive::Unreachable)?;
let type_id = layout_spec(env, builder, interner, layout)?;
let mode = update_mode.to_bytes();
let update_mode_var = UpdateModeVar(&mode);
@ -1238,13 +1146,7 @@ fn lowlevel_spec<'a>(
match interner.get(layout) {
Layout::Builtin(Builtin::List(element_layout)) => {
let type_id = layout_spec(
env,
builder,
interner,
element_layout,
&WhenRecursive::Unreachable,
)?;
let type_id = layout_spec(env, builder, interner, element_layout)?;
new_list(builder, block, type_id)
}
_ => unreachable!("empty array does not have a list layout"),
@ -1255,6 +1157,11 @@ fn lowlevel_spec<'a>(
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]];
@ -1287,8 +1194,7 @@ fn lowlevel_spec<'a>(
// TODO overly pessimstic
let arguments: Vec<_> = arguments.iter().map(|symbol| env.symbols[symbol]).collect();
let result_type =
layout_spec(env, builder, interner, layout, &WhenRecursive::Unreachable)?;
let result_type = layout_spec(env, builder, interner, layout)?;
builder.add_unknown_with(block, &arguments, result_type)
}
@ -1299,12 +1205,9 @@ fn recursive_tag_variant<'a>(
env: &mut Env<'a>,
builder: &mut impl TypeContext,
interner: &STLayoutInterner<'a>,
union_layout: &UnionLayout,
fields: &[InLayout<'a>],
) -> Result<TypeId> {
let when_recursive = WhenRecursive::Loop(*union_layout);
build_recursive_tuple_type(env, builder, interner, fields, &when_recursive)
build_recursive_tuple_type(env, builder, interner, fields)
}
fn recursive_variant_types<'a>(
@ -1325,23 +1228,11 @@ fn recursive_variant_types<'a>(
result = Vec::with_capacity(tags.len());
for tag in tags.iter() {
result.push(recursive_tag_variant(
env,
builder,
interner,
union_layout,
tag,
)?);
result.push(recursive_tag_variant(env, builder, interner, tag)?);
}
}
NonNullableUnwrapped(fields) => {
result = vec![recursive_tag_variant(
env,
builder,
interner,
union_layout,
fields,
)?];
result = vec![recursive_tag_variant(env, builder, interner, fields)?];
}
NullableWrapped {
nullable_id,
@ -1352,39 +1243,21 @@ fn recursive_variant_types<'a>(
let cutoff = *nullable_id as usize;
for tag in tags[..cutoff].iter() {
result.push(recursive_tag_variant(
env,
builder,
interner,
union_layout,
tag,
)?);
result.push(recursive_tag_variant(env, builder, interner, tag)?);
}
result.push(recursive_tag_variant(
env,
builder,
interner,
union_layout,
&[],
)?);
result.push(recursive_tag_variant(env, builder, interner, &[])?);
for tag in tags[cutoff..].iter() {
result.push(recursive_tag_variant(
env,
builder,
interner,
union_layout,
tag,
)?);
result.push(recursive_tag_variant(env, builder, interner, tag)?);
}
}
NullableUnwrapped {
nullable_id,
other_fields: fields,
} => {
let unit = recursive_tag_variant(env, builder, interner, union_layout, &[])?;
let other_type = recursive_tag_variant(env, builder, interner, union_layout, 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
@ -1432,13 +1305,7 @@ fn expr_spec<'a>(
let value_id = match tag_layout {
UnionLayout::NonRecursive(tags) => {
let variant_types = non_recursive_variant_types(
env,
builder,
interner,
tags,
&WhenRecursive::Unreachable,
)?;
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);
}
@ -1543,13 +1410,7 @@ fn expr_spec<'a>(
builder.add_get_tuple_field(block, value_id, *index as u32)
}
Array { elem_layout, elems } => {
let type_id = layout_spec(
env,
builder,
interner,
*elem_layout,
&WhenRecursive::Unreachable,
)?;
let type_id = layout_spec(env, builder, interner, *elem_layout)?;
let list = new_list(builder, block, type_id)?;
@ -1576,13 +1437,7 @@ fn expr_spec<'a>(
EmptyArray => match interner.get(layout) {
Layout::Builtin(Builtin::List(element_layout)) => {
let type_id = layout_spec(
env,
builder,
interner,
element_layout,
&WhenRecursive::Unreachable,
)?;
let type_id = layout_spec(env, builder, interner, element_layout)?;
new_list(builder, block, type_id)
}
_ => unreachable!("empty array does not have a list layout"),
@ -1615,7 +1470,7 @@ fn expr_spec<'a>(
with_new_heap_cell(builder, block, union_data)
}
RuntimeErrorFunction(_) => {
let type_id = layout_spec(env, builder, interner, layout, &WhenRecursive::Unreachable)?;
let type_id = layout_spec(env, builder, interner, layout)?;
builder.add_terminate(block, type_id)
}
@ -1647,9 +1502,8 @@ fn layout_spec<'a>(
builder: &mut impl TypeContext,
interner: &STLayoutInterner<'a>,
layout: InLayout<'a>,
when_recursive: &WhenRecursive,
) -> Result<TypeId> {
layout_spec_help(env, builder, interner, layout, when_recursive)
layout_spec_help(env, builder, interner, layout)
}
fn non_recursive_variant_types<'a>(
@ -1657,19 +1511,11 @@ fn non_recursive_variant_types<'a>(
builder: &mut impl TypeContext,
interner: &STLayoutInterner<'a>,
tags: &[&[InLayout<'a>]],
// If there is a recursive pointer latent within this layout, coming from a containing layout.
when_recursive: &WhenRecursive,
) -> Result<Vec<TypeId>> {
let mut result = Vec::with_capacity(tags.len());
for tag in tags.iter() {
result.push(build_tuple_type(
env,
builder,
interner,
tag,
when_recursive,
)?);
result.push(build_tuple_type(env, builder, interner, tag)?);
}
Ok(result)
@ -1680,22 +1526,17 @@ fn layout_spec_help<'a>(
builder: &mut impl TypeContext,
interner: &STLayoutInterner<'a>,
layout: InLayout<'a>,
when_recursive: &WhenRecursive,
) -> Result<TypeId> {
use Layout::*;
match interner.get(layout) {
Builtin(builtin) => builtin_spec(env, builder, interner, &builtin, when_recursive),
Builtin(builtin) => builtin_spec(env, builder, interner, &builtin),
Struct { field_layouts, .. } => {
build_recursive_tuple_type(env, builder, interner, field_layouts, when_recursive)
build_recursive_tuple_type(env, builder, interner, field_layouts)
}
LambdaSet(lambda_set) => {
layout_spec_help(env, builder, interner, lambda_set.runtime_representation())
}
LambdaSet(lambda_set) => layout_spec_help(
env,
builder,
interner,
lambda_set.runtime_representation(),
when_recursive,
),
Union(union_layout) => {
match union_layout {
UnionLayout::NonRecursive(&[]) => {
@ -1705,8 +1546,7 @@ fn layout_spec_help<'a>(
builder.add_tuple_type(&[])
}
UnionLayout::NonRecursive(tags) => {
let variant_types =
non_recursive_variant_types(env, builder, interner, tags, when_recursive)?;
let variant_types = non_recursive_variant_types(env, builder, interner, tags)?;
builder.add_union_type(&variant_types)
}
UnionLayout::Recursive(_)
@ -1724,29 +1564,21 @@ fn layout_spec_help<'a>(
}
Boxed(inner_layout) => {
let inner_type =
layout_spec_help(env, builder, interner, inner_layout, when_recursive)?;
let inner_type = layout_spec_help(env, builder, interner, 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(_) => 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);
RecursivePointer(union_layout) => match interner.get(union_layout) {
Layout::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))
}
},
Ok(builder.add_named_type(MOD_APP, type_name))
}
_ => internal_error!("somehow, a non-recursive layout is under a recursive pointer"),
},
}
}
@ -1756,7 +1588,6 @@ fn builtin_spec<'a>(
builder: &mut impl TypeContext,
interner: &STLayoutInterner<'a>,
builtin: &Builtin<'a>,
when_recursive: &WhenRecursive,
) -> Result<TypeId> {
use Builtin::*;
@ -1765,8 +1596,7 @@ fn builtin_spec<'a>(
Decimal | Float(_) => builder.add_tuple_type(&[]),
Str => str_type(builder),
List(element_layout) => {
let element_type =
layout_spec_help(env, builder, interner, *element_layout, when_recursive)?;
let element_type = layout_spec_help(env, builder, interner, *element_layout)?;
let cell = builder.add_heap_cell_type();
let bag = builder.add_bag_type(element_type)?;

View file

@ -1,11 +1,12 @@
[package]
name = "arena-pool"
version = "0.0.1"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
repository = "https://github.com/roc-lang/roc"
edition = "2021"
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" }

View file

@ -1,52 +1,55 @@
[package]
name = "roc_build"
version = "0.0.1"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
edition = "2021"
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_collections = { path = "../collections" }
roc_bitcode = { path = "../builtins/bitcode" }
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_collections = { path = "../collections" }
roc_constrain = { path = "../constrain" }
roc_unify = { path = "../unify" }
roc_solve_problem = { path = "../solve_problem" }
roc_mono = { path = "../mono" }
roc_load = { path = "../load" }
roc_target = { path = "../roc_target" }
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_gen_dev = { path = "../gen_dev", default-features = false }
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_error_macros = { path = "../../error_macros" }
roc_solve_problem = { path = "../solve_problem" }
roc_std = { path = "../../roc_std" }
roc_utils = { path = "../../utils" }
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" }
const_format.workspace = true
bumpalo.workspace = true
libloading.workspace = true
tempfile.workspace = true
target-lexicon.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 = "1.0.85"
serde_json.workspace = true
[features]
target-arm = []
target-aarch64 = ["roc_gen_dev/target-aarch64"]
target-arm = []
target-wasm32 = []
target-x86 = []
target-x86_64 = ["roc_gen_dev/target-x86_64"]
target-wasm32 = []
# This is used to enable fuzzing and sanitizers.
# Example use is describe here: https://github.com/bhansconnect/roc-fuzz

View file

@ -1,26 +1,18 @@
use crate::target::{arch_str, target_zig_str};
use const_format::concatcp;
use libloading::{Error, Library};
use roc_builtins::bitcode;
use roc_command_utils::{cargo, clang, get_lib_path, rustup, zig};
use roc_error_macros::internal_error;
use roc_mono::ir::OptLevel;
use roc_utils::{cargo, clang, zig};
use roc_utils::{get_lib_path, rustup};
use std::collections::HashMap;
use std::env;
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};
#[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 use roc_linker::LinkType;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum LinkingStrategy {
@ -60,134 +52,15 @@ pub fn link(
}
}
const PRECOMPILED_HOST_EXT: &str = "rh1"; // Short for "roc host version 1" (so we can change format in the future)
const WASM_TARGET_STR: &str = "wasm32";
const LINUX_X86_64_TARGET_STR: &str = "linux-x86_64";
const LINUX_ARM64_TARGET_STR: &str = "linux-arm64";
const MACOS_ARM64_TARGET_STR: &str = "macos-arm64";
const MACOS_X86_64_TARGET_STR: &str = "macos-x86_64";
const WINDOWS_X86_64_TARGET_STR: &str = "windows-x86_64";
const WINDOWS_X86_32_TARGET_STR: &str = "windows-x86_32";
const WIDNOWS_ARM64_TARGET_STR: &str = "windows-arm64";
pub const fn preprocessed_host_filename(target: &Triple) -> Option<&'static str> {
// Don't try to split this match off in a different function, it will not work with concatcp
match target {
Triple {
architecture: Architecture::Wasm32,
..
} => Some(concatcp!(WASM_TARGET_STR, '.', PRECOMPILED_HOST_EXT)),
Triple {
operating_system: OperatingSystem::Linux,
architecture: Architecture::X86_64,
..
} => Some(concatcp!(
LINUX_X86_64_TARGET_STR,
'.',
PRECOMPILED_HOST_EXT
)),
Triple {
operating_system: OperatingSystem::Linux,
architecture: Architecture::Aarch64(_),
..
} => Some(concatcp!(LINUX_ARM64_TARGET_STR, '.', PRECOMPILED_HOST_EXT)),
Triple {
operating_system: OperatingSystem::Darwin,
architecture: Architecture::Aarch64(_),
..
} => Some(concatcp!(MACOS_ARM64_TARGET_STR, '.', PRECOMPILED_HOST_EXT)),
Triple {
operating_system: OperatingSystem::Darwin,
architecture: Architecture::X86_64,
..
} => Some(concatcp!(
MACOS_X86_64_TARGET_STR,
'.',
PRECOMPILED_HOST_EXT
)),
Triple {
operating_system: OperatingSystem::Windows,
architecture: Architecture::X86_64,
..
} => Some(concatcp!(
WINDOWS_X86_64_TARGET_STR,
'.',
PRECOMPILED_HOST_EXT
)),
Triple {
operating_system: OperatingSystem::Windows,
architecture: Architecture::X86_32(_),
..
} => Some(concatcp!(
WINDOWS_X86_32_TARGET_STR,
'.',
PRECOMPILED_HOST_EXT
)),
Triple {
operating_system: OperatingSystem::Windows,
architecture: Architecture::Aarch64(_),
..
} => Some(concatcp!(
WIDNOWS_ARM64_TARGET_STR,
'.',
PRECOMPILED_HOST_EXT
)),
_ => None,
}
}
pub fn get_target_triple_str(target: &Triple) -> Option<&'static str> {
match target {
Triple {
architecture: Architecture::Wasm32,
..
} => Some(WASM_TARGET_STR),
Triple {
operating_system: OperatingSystem::Linux,
architecture: Architecture::X86_64,
..
} => Some(LINUX_X86_64_TARGET_STR),
Triple {
operating_system: OperatingSystem::Linux,
architecture: Architecture::Aarch64(_),
..
} => Some(LINUX_ARM64_TARGET_STR),
Triple {
operating_system: OperatingSystem::Darwin,
architecture: Architecture::Aarch64(_),
..
} => Some(MACOS_ARM64_TARGET_STR),
Triple {
operating_system: OperatingSystem::Darwin,
architecture: Architecture::X86_64,
..
} => Some(MACOS_X86_64_TARGET_STR),
Triple {
operating_system: OperatingSystem::Windows,
architecture: Architecture::X86_64,
..
} => Some(WINDOWS_X86_64_TARGET_STR),
Triple {
operating_system: OperatingSystem::Windows,
architecture: Architecture::X86_32(_),
..
} => Some(WINDOWS_X86_32_TARGET_STR),
Triple {
operating_system: OperatingSystem::Windows,
architecture: Architecture::Aarch64(_),
..
} => Some(WIDNOWS_ARM64_TARGET_STR),
_ => None,
}
}
/// Same format as the precompiled host filename, except with a file extension like ".o" or ".obj"
pub fn legacy_host_filename(target: &Triple) -> Option<String> {
let os = roc_target::OperatingSystem::from(target.operating_system);
let ext = os.object_file_ext();
Some(preprocessed_host_filename(target)?.replace(PRECOMPILED_HOST_EXT, ext))
Some(
roc_linker::preprocessed_host_filename(target)?
.replace(roc_linker::PRECOMPILED_HOST_EXT, ext),
)
}
fn find_zig_str_path() -> PathBuf {
@ -681,7 +554,7 @@ pub fn rebuild_host(
let env_cpath = env::var("CPATH").unwrap_or_else(|_| "".to_string());
let builtins_host_tempfile =
bitcode::host_tempfile().expect("failed to write host builtins object to tempfile");
roc_bitcode::host_tempfile().expect("failed to write host builtins object to tempfile");
if zig_host_src.exists() {
// Compile host.zig
@ -752,14 +625,6 @@ pub fn rebuild_host(
// Compile and link Cargo.toml, if it exists
let cargo_dir = platform_main_roc.parent().unwrap();
let cargo_out_dir = cargo_dir.join("target").join(
if matches!(opt_level, OptLevel::Optimize | OptLevel::Size) {
"release"
} else {
"debug"
},
);
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
@ -793,11 +658,20 @@ pub fn rebuild_host(
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);
std::fs::copy(&exe_path, &host_dest).unwrap();
if let Err(e) = std::fs::copy(&exe_path, &host_dest) {
panic!(
"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.
@ -943,6 +817,63 @@ pub fn rebuild_host(
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 => panic!("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<DirEntry> {
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
@ -1502,8 +1433,8 @@ pub fn preprocess_host_wasm32(host_input_path: &Path, preprocessed_host_path: &P
(but seems to be an unofficial API)
*/
let builtins_host_tempfile =
bitcode::host_wasm_tempfile().expect("failed to write host builtins object to tempfile");
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 = &[

View file

@ -1,17 +1,36 @@
use crate::link::{
legacy_host_filename, 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_llvm::llvm::build::{module_from_builtins, LlvmBackendMode};
use roc_gen_llvm::llvm::externs::add_default_roc_externs;
use roc_load::{EntryPoint, ExpectMetadata, LoadedModule, MonomorphizedModule};
use roc_load::{
EntryPoint, ExecutionMode, ExpectMetadata, LoadConfig, LoadMonomorphizedError, LoadedModule,
LoadingProblem, MonomorphizedModule, Threading,
};
use roc_mono::ir::{OptLevel, SingleEntryPoint};
use roc_reporting::cli::{report_problems, Problems};
use roc_packaging::cache::RocCacheDir;
use roc_reporting::{
cli::{report_problems, Problems},
report::{RenderTarget, DEFAULT_PALETTE},
};
use roc_target::TargetInfo;
use std::ffi::OsStr;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
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 code_gen: Duration,
@ -56,7 +75,7 @@ impl Deref for CodeObject {
#[derive(Debug, Clone, Copy)]
pub enum CodeGenBackend {
Assembly,
Llvm,
Llvm(LlvmBackendMode),
Wasm,
}
@ -79,6 +98,10 @@ pub fn gen_from_mono_module<'a>(
preprocessed_host_path: &Path,
wasm_dev_stack_bytes: Option<u32>,
) -> 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::Assembly => gen_from_mono_module_dev(
arena,
@ -87,12 +110,18 @@ pub fn gen_from_mono_module<'a>(
preprocessed_host_path,
wasm_dev_stack_bytes,
),
CodeGenBackend::Llvm => {
gen_from_mono_module_llvm(arena, loaded, roc_file_path, target, code_gen_options)
CodeGenBackend::Llvm(backend_mode) => {
gen_from_mono_module_llvm(arena, loaded, path, target, opt, backend_mode, debug)
}
CodeGenBackend::Wasm => {
// emit wasm via the llvm backend
gen_from_mono_module_llvm(arena, loaded, roc_file_path, target, code_gen_options)
let backend_mode = match code_gen_options.opt_level {
OptLevel::Development => LlvmBackendMode::BinaryDev,
OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => LlvmBackendMode::Binary,
};
gen_from_mono_module_llvm(arena, loaded, path, target, opt, backend_mode, debug)
}
}
}
@ -105,7 +134,9 @@ fn gen_from_mono_module_llvm<'a>(
mut loaded: MonomorphizedModule<'a>,
roc_file_path: &Path,
target: &target_lexicon::Triple,
code_gen_options: CodeGenOptions,
opt_level: OptLevel,
backend_mode: LlvmBackendMode,
emit_debug_info: bool,
) -> GenFromMono<'a> {
use crate::target::{self, convert_opt_level};
use inkwell::attributes::{Attribute, AttributeLoc};
@ -155,12 +186,6 @@ fn gen_from_mono_module_llvm<'a>(
}
}
let CodeGenOptions {
backend: _,
opt_level,
emit_debug_info,
} = code_gen_options;
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);
@ -175,12 +200,14 @@ fn gen_from_mono_module_llvm<'a>(
interns: loaded.interns,
module,
target_info,
mode: match opt_level {
OptLevel::Development => LlvmBackendMode::BinaryDev,
OptLevel::Normal | OptLevel::Size | OptLevel::Optimize => LlvmBackendMode::Binary,
},
mode: backend_mode,
exposed_to_host: loaded.exposed_to_host.values.keys().copied().collect(),
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
@ -208,6 +235,7 @@ fn gen_from_mono_module_llvm<'a>(
loaded.procedures,
entry_point,
Some(&app_ll_file),
&loaded.glue_layouts,
);
env.dibuilder.finalize();
@ -484,7 +512,7 @@ fn gen_from_mono_module_dev_wasm32<'a>(
let exposed_to_host = loaded
.exposed_to_host
.values
.top_level_values
.keys()
.copied()
.collect::<MutSet<_>>();
@ -555,7 +583,7 @@ fn gen_from_mono_module_dev_assembly<'a>(
let env = roc_gen_dev::Env {
arena,
module_id,
exposed_to_host: exposed_to_host.values.keys().copied().collect(),
exposed_to_host: exposed_to_host.top_level_values.keys().copied().collect(),
lazy_literals,
generate_allocators,
};
@ -579,3 +607,675 @@ fn gen_from_mono_module_dev_assembly<'a>(
},
)
}
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<i32> {
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<i32> {
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,
};
LoadConfig {
target_info,
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<u32>,
roc_cache_dir: RocCacheDir<'_>,
load_config: LoadConfig,
) -> Result<BuiltFile<'a>, 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,
)
}
#[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,
linking_strategy: LinkingStrategy,
prebuilt_requested: bool,
wasm_dev_stack_bytes: Option<u32>,
loaded: roc_load::MonomorphizedModule<'a>,
compilation_start: Instant,
) -> Result<BuiltFile<'a>, 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!(),
};
// 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 {
platform_main_roc.with_file_name(legacy_host_filename(target).unwrap())
}
} else {
platform_main_roc.with_file_name(roc_linker::preprocessed_host_filename(target).unwrap())
};
// For example, if we're loading the platform from a URL, it's automatically prebuilt
// even if the --prebuilt-platform=true CLI flag wasn't set.
let is_platform_prebuilt = prebuilt_requested || loaded.uses_prebuilt_platform;
let cwd = app_module_path.parent().unwrap();
let mut output_exe_path = cwd.join(&*loaded.output_path);
if let Some(extension) = operating_system.executable_file_ext() {
output_exe_path.set_extension(extension);
}
// 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<u128>),
}
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 {} ms\n",
rebuild_duration
);
}
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 Assembly from Mono IR",
code_gen_timing.code_gen,
);
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 {} ms\n",
rebuild_duration
);
}
}
// 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 app_o_file = tempfile::Builder::new()
.prefix("roc_app")
.suffix(&format!(".{}", operating_system.object_file_ext()))
.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 = match prebuilt_requested {
true => "Because I was run with --prebuilt-platform=true, ",
false => "",
};
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 with --prebuilt-platform=false
"#
),
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<String>,
) -> std::thread::JoinHandle<u128> {
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: 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<BuiltFile<'a>, 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,
)
}

View file

@ -1,23 +1,17 @@
[package]
name = "roc_builtins"
version = "0.0.1"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
edition = "2021"
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_region = { path = "../region" }
roc_module = { path = "../module" }
roc_region = { path = "../region" }
roc_target = { path = "../roc_target" }
roc_utils = { path = "../../utils" }
tempfile.workspace = true
[build-dependencies]
# dunce can be removed once ziglang/zig#5109 is fixed
dunce = "1.0.3"
roc_utils = { path = "../../utils" }
[target.'cfg(target_os = "macos")'.build-dependencies]
tempfile.workspace = true

View file

@ -0,0 +1,19 @@
[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" }
# dunce can be removed once ziglang/zig#5109 is fixed
dunce = "1.0.3"
[target.'cfg(target_os = "macos")'.build-dependencies]
tempfile.workspace = true

View file

@ -0,0 +1,16 @@
[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" }
# dunce can be removed once ziglang/zig#5109 is fixed
dunce = "1.0.3"
[target.'cfg(target_os = "macos")'.build-dependencies]
tempfile.workspace = true

View file

@ -0,0 +1,154 @@
use roc_command_utils::{pretty_command_string, zig};
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-i386", "builtins-i386");
generate_bc_file(&bitcode_path, "ir-x86_64", "builtins-x86_64");
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(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("./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 {
panic!("{} 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")
{
panic!("{} failed with:\n\n {}\n\nWorkaround:\n\n Re-run the cargo command that triggered this build.\n\n", command_str, error_str);
} else {
panic!("{} failed with:\n\n {}\n", 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(())
}

View file

@ -0,0 +1 @@

View file

@ -1,11 +1,9 @@
use roc_utils::zig;
use std::env;
use roc_command_utils::{pretty_command_string, zig};
use std::fs;
use std::io;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::str;
use std::{env, path::PathBuf, process::Command};
#[cfg(target_os = "macos")]
use tempfile::tempdir;
@ -18,8 +16,7 @@ fn main() {
// "." 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");
let bitcode_path = dunce::canonicalize(Path::new(".")).unwrap();
// workaround for github.com/ziglang/zig/issues/9711
#[cfg(target_os = "macos")]
@ -27,22 +24,6 @@ fn main() {
#[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");
generate_bc_file(
&bitcode_path,
"ir-windows-x86_64",
"builtins-windows-x86_64",
);
// OBJECT FILES
#[cfg(windows)]
const BUILTINS_HOST_FILE: &str = "builtins-host.obj";
@ -107,38 +88,13 @@ fn generate_object_file(bitcode_path: &Path, zig_object: &str, object_file_name:
}
}
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");
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()).join("bitcode");
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/bitcode dir.");
fs::create_dir_all(&dir).expect("Failed to make $OUT_DIR/ dir.");
dir
}
@ -192,7 +148,7 @@ fn cp_unless_zig_cache(src_dir: &Path, target_dir: &Path) -> io::Result<()> {
}
fn run_command(mut command: Command, flaky_fail_counter: usize) {
let command_str = roc_utils::pretty_command_string(&command);
let command_str = pretty_command_string(&command);
let command_str = command_str.to_string_lossy();
let output_result = command.output();

View file

@ -921,8 +921,8 @@ test "toStr: 123.1111111" {
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();
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());
@ -931,8 +931,8 @@ test "toStr: 123.1111111111111 (big str)" {
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();
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());
@ -941,8 +941,8 @@ test "toStr: 123.111111111111444444 (max number of decimal places)" {
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();
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());
@ -951,8 +951,8 @@ test "toStr: 12345678912345678912.111111111111111111 (max number of digits)" {
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();
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());
@ -961,8 +961,8 @@ test "toStr: std.math.maxInt" {
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();
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());
@ -1047,8 +1047,8 @@ test "div: 10 / 3" {
var denom: RocDec = RocDec.fromU64(3);
var roc_str = RocStr.init("3.333333333333333333", 20);
errdefer roc_str.deinit();
defer roc_str.deinit();
errdefer roc_str.decref();
defer roc_str.decref();
var res: RocDec = RocDec.fromStr(roc_str).?;

View file

@ -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<NamedTempFile> {
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<NamedTempFile> {
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<NamedTempFile> {
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<NamedTempFile> {
#[cfg(unix)]
{
host_unix_tempfile()
}
#[cfg(windows)]
{
host_windows_tempfile()
}
#[cfg(not(any(windows, unix)))]
{
unreachable!()
}
}

View file

@ -15,21 +15,45 @@ 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 };
const SEAMLESS_SLICE_BIT: usize =
@bitCast(usize, @as(isize, std.math.minInt(isize)));
pub const RocList = extern struct {
bytes: ?[*]u8,
length: usize,
capacity: 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 @bitCast(isize, 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 @bitCast(usize, @bitCast(isize, 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 = 0 };
return RocList{ .bytes = null, .length = 0, .capacity_or_ref_ptr = 0 };
}
pub fn eql(self: RocList, other: RocList) bool {
@ -75,8 +99,21 @@ pub const RocList = extern struct {
return list;
}
pub fn deinit(self: RocList, comptime T: type) void {
utils.decref(self.bytes, self.capacity, @alignOf(T));
// 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 = @ptrToInt(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 @intToPtr(?[*]u8, 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 {
@ -88,7 +125,7 @@ pub const RocList = extern struct {
}
fn refcountMachine(self: RocList) usize {
if (self.capacity == 0) {
if (self.getCapacity() == 0 and !self.isSeamlessSlice()) {
// the zero-capacity is Clone, copying it will not leak memory
return utils.REFCOUNT_ONE;
}
@ -110,12 +147,15 @@ pub const RocList = extern struct {
}
pub fn makeUnique(self: RocList, alignment: u32, element_width: usize) RocList {
if (self.isEmpty()) {
if (self.isUnique()) {
return self;
}
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
@ -127,9 +167,8 @@ pub const RocList = extern struct {
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);
// NOTE we fuse an increment of all keys/values with a decrement of the input list.
self.decref(alignment);
return new_list;
}
@ -148,7 +187,24 @@ pub const RocList = extern struct {
return RocList{
.bytes = utils.allocateWithRefcount(data_bytes, alignment),
.length = length,
.capacity = capacity,
.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,
};
}
@ -159,13 +215,14 @@ pub const RocList = extern struct {
element_width: usize,
) RocList {
if (self.bytes) |source_ptr| {
if (self.isUnique()) {
if (self.capacity >= new_length) {
return RocList{ .bytes = self.bytes, .length = new_length, .capacity = self.capacity };
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(self.capacity, new_length, element_width);
const new_source = utils.unsafeReallocate(source_ptr, alignment, self.len(), new_capacity, element_width);
return RocList{ .bytes = new_source, .length = new_length, .capacity = new_capacity };
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);
@ -193,7 +250,7 @@ pub const RocList = extern struct {
@memset(dest_ptr + old_length * element_width, 0, delta_length * element_width);
}
utils.decref(self.bytes, old_length * element_width, alignment);
self.decref(alignment);
return result;
}
@ -428,7 +485,7 @@ pub fn listReserve(
update_mode: UpdateMode,
) callconv(.C) RocList {
const old_length = list.len();
if ((update_mode == .InPlace or list.isUnique()) and list.capacity >= list.len() + spare) {
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);
@ -437,6 +494,31 @@ pub fn listReserve(
}
}
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, source_ptr, old_length * element_width);
}
list.decref(alignment);
return output;
}
}
pub fn listAppendUnsafe(
list: RocList,
element: Opaque,
@ -462,10 +544,12 @@ fn listAppend(list: RocList, alignment: u32, element: Opaque, element_width: usi
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);
// 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 (output.bytes) |target| {
if (with_capacity.bytes) |target| {
var i: usize = old_length;
while (i > 0) {
@ -481,7 +565,7 @@ pub fn listPrepend(list: RocList, alignment: u32, element: Opaque, element_width
}
}
return output;
return with_capacity;
}
pub fn listSwap(
@ -522,19 +606,20 @@ pub fn listSublist(
) callconv(.C) RocList {
const size = list.len();
if (len == 0 or start >= size) {
if (list.isUnique()) {
// 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);
}
var output = list;
output.length = 0;
return output;
// 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();
}
@ -557,26 +642,20 @@ pub fn listSublist(
dec(element);
}
if (list.isUnique()) {
if (start == 0 and list.isUnique()) {
var output = list;
output.length = keep_len;
if (start == 0) {
return output;
} else {
// We want memmove due to aliasing. Zig does not expose it directly.
// Instead use copy which can write to aliases as long as the dest is before the source.
mem.copy(u8, source_ptr[0 .. keep_len * element_width], source_ptr[start * element_width .. (start + keep_len) * element_width]);
return output;
}
} else {
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;
} else {
const list_ref_ptr = (@ptrToInt(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,
};
}
}
@ -590,9 +669,17 @@ pub fn listDropAt(
drop_index: usize,
dec: Dec,
) callconv(.C) RocList {
if (list.bytes) |source_ptr| {
const size = list.len();
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;
}
@ -607,7 +694,7 @@ pub fn listDropAt(
// 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);
list.decref(alignment);
return RocList.empty();
}
@ -637,7 +724,7 @@ pub fn listDropAt(
const tail_size = (size - drop_index - 1) * element_width;
@memcpy(tail_target, tail_source, tail_size);
utils.decref(list.bytes, size * element_width, alignment);
list.decref(alignment);
return output;
} else {
@ -747,11 +834,13 @@ fn swapElements(source_ptr: [*]u8, element_width: usize, index_1: usize, index_2
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.capacity == 0) {
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.deinit(usize);
list_b.decref(alignment);
return list_a;
}
@ -766,7 +855,7 @@ pub fn listConcat(list_a: RocList, list_b: RocList, alignment: u32, element_widt
@memcpy(source_a + list_a.len() * element_width, source_b, list_b.len() * element_width);
// decrement list b.
utils.decref(source_b, list_b.len(), alignment);
list_b.decref(alignment);
return resized_list_a;
} else if (list_b.isUnique()) {
@ -787,7 +876,7 @@ pub fn listConcat(list_a: RocList, list_b: RocList, alignment: u32, element_widt
@memcpy(source_b, source_a, byte_count_a);
// decrement list a.
utils.decref(source_a, list_a.len(), alignment);
list_a.decref(alignment);
return resized_list_b;
}
@ -804,8 +893,8 @@ pub fn listConcat(list_a: RocList, list_b: RocList, alignment: u32, element_widt
@memcpy(target + list_a.len() * element_width, source_b, list_b.len() * element_width);
// decrement list a and b.
utils.decref(source_a, list_a.len(), alignment);
utils.decref(source_b, list_b.len(), alignment);
list_a.decref(alignment);
list_b.decref(alignment);
return output;
}
@ -868,20 +957,32 @@ pub fn listIsUnique(
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 = @ptrCast([*]u8, nonUnique.bytes);
const ptr_width = @sizeOf(usize);
const refcount_ptr = @ptrCast([*]isize, @alignCast(ptr_width, bytes) - ptr_width);
utils.increfC(&refcount_ptr[0], 1);
defer nonUnique.deinit(u8); // listConcat will dec the other refcount
defer nonUnique.decref(@sizeOf(u8)); // listConcat will dec the other refcount
var unique = RocList.fromSlice(u8, ([_]u8{ 2, 3, 4 })[0..]);
defer unique.deinit(u8);
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.deinit(u8);
defer wanted.decref(@sizeOf(u8));
try expect(concatted.eql(wanted));
}

View file

@ -54,6 +54,9 @@ comptime {
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
@ -67,6 +70,8 @@ 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");
inline for (INTEGERS) |T, i| {
num.exportPow(T, ROC_BUILTINS ++ "." ++ NUM ++ ".pow_int.");
@ -86,6 +91,10 @@ comptime {
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.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| {
@ -138,7 +147,6 @@ comptime {
exportStrFn(str.getScalarUnsafe, "get_scalar_unsafe");
exportStrFn(str.appendScalar, "append_scalar");
exportStrFn(str.strToUtf8C, "to_utf8");
exportStrFn(str.fromUtf8C, "from_utf8");
exportStrFn(str.fromUtf8RangeC, "from_utf8_range");
exportStrFn(str.repeat, "repeat");
exportStrFn(str.strTrim, "trim");
@ -147,6 +155,8 @@ comptime {
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.");

View file

@ -236,6 +236,24 @@ fn bytesToU32(arg: RocList, position: usize) u32 {
return @bitCast(u32, [_]u8{ bytes[position], bytes[position + 1], bytes[position + 2], bytes[position + 3] });
}
pub fn bytesToU64C(arg: RocList, position: usize) callconv(.C) u64 {
return @call(.{ .modifier = always_inline }, bytesToU64, .{ arg, position });
}
fn bytesToU64(arg: RocList, position: usize) u64 {
const bytes = @ptrCast([*]const u8, arg.bytes);
return @bitCast(u64, [_]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(.{ .modifier = always_inline }, bytesToU128, .{ arg, position });
}
fn bytesToU128(arg: RocList, position: usize) u128 {
const bytes = @ptrCast([*]const u8, arg.bytes);
return @bitCast(u128, [_]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 addWithOverflow(comptime T: type, self: T, other: T) WithOverflow(T) {
switch (@typeInfo(T)) {
.Int => {
@ -460,3 +478,30 @@ pub fn exportMulOrPanic(comptime T: type, comptime W: type, comptime name: []con
}.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(T, 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(T, 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(T, self));
}
}.func;
@export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong });
}

File diff suppressed because it is too large Load diff

View file

@ -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()]);
}
}

View file

@ -32,6 +32,8 @@ Eq has
## cannot derive `isEq` for types that contain functions.
isEq : a, a -> Bool | a has Eq
## Represents the boolean true and false using an opaque type.
## `Bool` implements the `Eq` ability.
Bool := [True, False] has [Eq { isEq: boolIsEq }]
boolIsEq = \@Bool b1, @Bool b2 -> structuralEq b1 b2
@ -49,23 +51,27 @@ false = @Bool False
## 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
## ```
## 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 Note** that in Roc the `&&` and `||` work the same way as any
## ## 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
##
## ```
## 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.
@ -74,14 +80,17 @@ 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
## ```
##
## 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
##
## **Performance Note** that in Roc the `&&` and `||` work the same way as any
## 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
@ -89,9 +98,10 @@ 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
## ```
## 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`
@ -101,10 +111,11 @@ not : Bool -> Bool
##
## **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"
## ```
## expect (Bool.isNotEq Bool.false Bool.true) == Bool.true
## expect (Bool.false != Bool.false) == Bool.false
## expect "Apples" != "Oranges"
## ```
isNotEq : a, a -> Bool | a has Eq
isNotEq = \a, b -> structuralNotEq a b

View file

@ -6,13 +6,15 @@ interface Box
## 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"
## ```
## 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"
## ```
## 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

View file

@ -23,6 +23,7 @@ interface Decode
string,
list,
record,
tuple,
custom,
decodeWith,
fromBytesPartial,
@ -43,6 +44,7 @@ interface Decode
I32,
I64,
I128,
Nat,
F32,
F64,
Dec,
@ -76,8 +78,24 @@ DecoderFormatting has
bool : Decoder Bool fmt | fmt has DecoderFormatting
string : Decoder Str fmt | fmt has DecoderFormatting
list : Decoder elem fmt -> Decoder (List elem) fmt | fmt has 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 | fmt has 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 | fmt has DecoderFormatting
custom : (List U8, fmt -> DecodeResult val) -> Decoder val fmt | fmt has DecoderFormatting
custom = \decode -> @Decoder decode

View file

@ -34,7 +34,7 @@ interface Dict
## A [dictionary](https://en.wikipedia.org/wiki/Associative_array) that lets you
## associate keys with values.
##
## ### Inserting
## ## Inserting
##
## The most basic way to use a dictionary is to start with an empty one and
## then:
@ -45,16 +45,16 @@ interface Dict
##
## 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
## ```
## 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.
@ -63,16 +63,16 @@ interface Dict
## 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
## ## Removing
##
## We can remove an element from the dictionary, like so:
##
## populationByCity
## |> Dict.remove "Philadelphia"
## |> Dict.keys
## ==
## ["London", "Amsterdam", "Shanghai", "Delhi"]
##
## ```
## 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]
@ -100,6 +100,9 @@ Dict k v := {
} | k has Hash & Eq
## Return an empty dictionary.
## ```
## emptyDict = Dict.empty {}
## ```
empty : {} -> Dict k v | k has Hash & Eq
empty = \{} ->
@Dict {
@ -110,6 +113,13 @@ empty = \{} ->
}
## 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 k v -> Nat | k has Hash & Eq
capacity = \@Dict { dataIndices } ->
cap = List.len dataIndices
@ -125,41 +135,55 @@ withCapacity = \_ ->
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")
## ```
## expect
## Dict.single "A" "B"
## |> Bool.isEq (Dict.insert (Dict.empty {}) "A" "B")
## ```
single : k, v -> Dict k v | k has 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 [T 1 "One", T 2 "Two", T 3 "Three", T 4 "Four"])
## ```
## expect
## Dict.single 1 "One"
## |> Dict.insert 2 "Two"
## |> Dict.insert 3 "Three"
## |> Dict.insert 4 "Four"
## |> Bool.isEq (Dict.fromList [T 1 "One", T 2 "Two", T 3 "Three", T 4 "Four"])
## ```
fromList : List (T k v) -> Dict k v | k has 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, T 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
## ```
## 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 k v -> Nat | k has Hash & Eq
len = \@Dict { size } ->
size
## 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 | k has Hash & Eq
clear = \@Dict { metadata, dataIndices, data } ->
cap = List.len dataIndices
@ -180,13 +204,14 @@ clear = \@Dict { metadata, dataIndices, data } ->
## 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
## ```
## 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 | k has Hash & Eq
walk = \@Dict { data }, initialState, transform ->
List.walk data initialState (\state, T k v -> transform state k v)
@ -202,20 +227,38 @@ walk = \@Dict { data }, initialState, transform ->
##
## 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 | k has Hash & Eq
walkUntil = \@Dict { data }, initialState, transform ->
List.walkUntil data initialState (\state, T 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"
##
## 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
## expect Dict.get dictionary 1 == Ok "Apple"
## expect Dict.get dictionary 2000 == Err KeyNotFound
## ```
get : Dict k v, k -> Result v [KeyNotFound] | k has Hash & Eq
get = \@Dict { metadata, dataIndices, data }, key ->
hashKey =
@ -237,12 +280,13 @@ get = \@Dict { metadata, dataIndices, data }, key ->
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
## ```
## expect
## Dict.empty {}
## |> Dict.insert 1234 "5678"
## |> Dict.contains 1234
## |> Bool.isEq Bool.true
## ```
contains : Dict k v, k -> Bool | k has Hash & Eq
contains = \@Dict { metadata, dataIndices, data }, key ->
hashKey =
@ -261,12 +305,13 @@ contains = \@Dict { metadata, dataIndices, data }, key ->
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)
## ```
## expect
## Dict.empty {}
## |> Dict.insert "Apples" 12
## |> Dict.get "Apples"
## |> Bool.isEq (Ok 12)
## ```
insert : Dict k v, k, v -> Dict k v | k has Hash & Eq
insert = \@Dict { metadata, dataIndices, data, size }, key, value ->
hashKey =
@ -305,13 +350,14 @@ insert = \@Dict { metadata, dataIndices, data, size }, key, value ->
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
## ```
## expect
## Dict.empty {}
## |> Dict.insert "Some" "Value"
## |> Dict.remove "Some"
## |> Dict.len
## |> Bool.isEq 0
## ```
remove : Dict k v, k -> Dict k v | k has Hash & Eq
remove = \@Dict { metadata, dataIndices, data, size }, key ->
# TODO: change this from swap remove to tombstone and test is performance is still good.
@ -345,16 +391,17 @@ remove = \@Dict { metadata, dataIndices, data, size }, key ->
## performance optimisation 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
##
## 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 {}
## 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 | k has Hash & Eq
update = \dict, key, alter ->
# TODO: look into optimizing by merging substeps and reducing lookups.
@ -369,42 +416,45 @@ update = \dict, key, alter ->
## 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 [T 1 "One", T 2 "Two", T 3 "Three", T 4 "Four"]
## ```
## expect
## Dict.single 1 "One"
## |> Dict.insert 2 "Two"
## |> Dict.insert 3 "Three"
## |> Dict.insert 4 "Four"
## |> Dict.toList
## |> Bool.isEq [T 1 "One", T 2 "Two", T 3 "Three", T 4 "Four"]
## ```
toList : Dict k v -> List (T k v) | k has 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]
## ```
## 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 | k has Hash & Eq
keys = \@Dict { data } ->
List.map data (\T 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"]
## ```
## 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 | k has Hash & Eq
values = \@Dict { data } ->
List.map data (\T _ v -> v)
@ -414,24 +464,25 @@ values = \@Dict { data } ->
## 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"
##
## 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"
##
## 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"
##
## 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
## expect
## Dict.insertAll first second == expected
## ```
insertAll : Dict k v, Dict k v -> Dict k v | k has Hash & Eq
insertAll = \xs, ys ->
walk ys xs insert
@ -441,18 +492,19 @@ insertAll = \xs, ys ->
## 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"
##
## 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"
##
## 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
## expect Dict.keepShared first second == first
## ```
keepShared : Dict k v, Dict k v -> Dict k v | k has Hash & Eq
keepShared = \xs, ys ->
walk
@ -469,21 +521,22 @@ keepShared = \xs, ys ->
## 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"
##
## 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..."
##
## second =
## Dict.single 3 "Remove Me"
## |> Dict.insert 4 "I do nothing..."
## expected =
## Dict.single 1 "Keep Me"
## |> Dict.insert 2 "And Me"
##
## expected =
## Dict.single 1 "Keep Me"
## |> Dict.insert 2 "And Me"
##
## expect Dict.removeAll first second == expected
## expect Dict.removeAll first second == expected
## ```
removeAll : Dict k v, Dict k v -> Dict k v | k has Hash & Eq
removeAll = \xs, ys ->
walk ys xs (\state, k, _ -> remove state k)
@ -851,7 +904,7 @@ LowLevelHasher := { originalSeed : U64, state : U64 } has [
# TODO hide behind an InternalList.roc module
listGetUnsafe : List a, Nat -> a
createLowLevelHasher : { seed ?U64 } -> LowLevelHasher
createLowLevelHasher : { seed ? U64 } -> LowLevelHasher
createLowLevelHasher = \{ seed ? 0x526F_6352_616E_643F } ->
@LowLevelHasher { originalSeed: seed, state: seed }
@ -1220,3 +1273,25 @@ expect
|> 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

View file

@ -22,6 +22,7 @@ interface Encode
list,
record,
tag,
tuple,
custom,
appendWith,
append,
@ -69,6 +70,7 @@ EncoderFormatting has
string : Str -> Encoder fmt | fmt has EncoderFormatting
list : List elem, (elem -> Encoder fmt) -> Encoder fmt | fmt has EncoderFormatting
record : List { key : Str, value : Encoder fmt } -> Encoder fmt | fmt has EncoderFormatting
tuple : List (Encoder fmt) -> Encoder fmt | fmt has EncoderFormatting
tag : Str, List (Encoder fmt) -> Encoder fmt | fmt has EncoderFormatting
custom : (List U8, fmt -> List U8) -> Encoder fmt | fmt has EncoderFormatting

View file

@ -9,6 +9,7 @@ interface Hash
addU32,
addU64,
addU128,
hashBool,
hashI8,
hashI16,
hashI32,
@ -20,7 +21,7 @@ interface Hash
hashList,
hashUnordered,
] imports [
Bool.{ isEq },
Bool.{ Bool, isEq },
List,
Str,
Num.{ U8, U16, U32, U64, U128, I8, I16, I32, I64, I128, Nat },
@ -70,6 +71,12 @@ hashList = \hasher, lst ->
List.walk lst hasher \accumHasher, elem ->
hash accumHasher elem
## Adds a single [Bool] to a hasher.
hashBool : a, Bool -> a | a has 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 | a has Hasher
hashI8 = \hasher, n -> addU8 hasher (Num.toU8 n)

View file

@ -1,3 +1,40 @@
## JSON is a data format that is easy for humans to read and write. It is
## commonly used to exhange data between two systems such as a server and a
## client (e.g. web browser).
##
## This module implements functionality to serialise and de-serialise Roc types
## to and from JSON data. Using the `Encode` and `Decode` builtins this process
## can be achieved without the need to write custom encoder and decoder functions
## to parse UTF-8 strings.
##
## Here is a basic example which shows how to parse a JSON record into a Roc
## type named `Language` which includes a `name` field. The JSON string is
## decoded and then the field is encoded back into a UTF-8 string.
##
## ```
## Language : {
## name : Str,
## }
##
## jsonStr = Str.toUtf8 "{\"name\":\"Röc Lang\"}"
##
## result : Result Language _
## result =
## jsonStr
## |> Decode.fromBytes fromUtf8 # returns `Ok {name : "Röc Lang"}`
##
## name =
## decodedValue <- Result.map result
##
## Encode.toBytes decodedValue.name toUtf8
##
## expect name == Ok (Str.toUtf8 "\"Röc Lang\"")
## ```
##
## **Note:** This module is likely to be moved out of the builtins in future.
## It is currently located here to facilitate development of the Abilities
## language feature and testing. You are welcome to use this module, just note
## that it will be moved into a package in a future update.
interface Json
exposes [
Json,
@ -7,6 +44,7 @@ interface Json
imports [
List,
Str,
Result.{ Result },
Encode,
Encode.{
Encoder,
@ -37,6 +75,8 @@ interface Json
Result,
]
## An opaque type with the `EncoderFormatting` and
## `DecoderFormatting` abilities.
Json := {} has [
EncoderFormatting {
u8: encodeU8,
@ -56,6 +96,7 @@ Json := {} has [
string: encodeString,
list: encodeList,
record: encodeRecord,
tuple: encodeTuple,
tag: encodeTag,
},
DecoderFormatting {
@ -76,11 +117,14 @@ Json := {} has [
string: decodeString,
list: decodeList,
record: decodeRecord,
tuple: decodeTuple,
},
]
## Returns a JSON `Decoder`
toUtf8 = @Json {}
## Returns a JSON `Encoder`
fromUtf8 = @Json {}
numToBytes = \n ->
@ -165,6 +209,25 @@ encodeRecord = \fields ->
List.append bytesWithRecord (Num.toU8 '}')
encodeTuple = \elems ->
Encode.custom \bytes, @Json {} ->
writeTuple = \{ buffer, elemsLeft }, elemEncoder ->
bufferWithElem =
appendWith buffer elemEncoder (@Json {})
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 ']')
encodeTag = \name, payload ->
Encode.custom \bytes, @Json {} ->
# Idea: encode `A v1 v2` as `{"A": [v1, v2]}`
@ -191,16 +254,42 @@ encodeTag = \name, payload ->
List.append bytesWithPayload (Num.toU8 ']')
|> List.append (Num.toU8 '}')
isEscapeSequence : U8, U8 -> Bool
isEscapeSequence = \a, b ->
when P a b is
P '\\' 'b' -> Bool.true # Backspace
P '\\' 'f' -> Bool.true # Form feed
P '\\' 'n' -> Bool.true # Newline
P '\\' 'r' -> Bool.true # Carriage return
P '\\' 't' -> Bool.true # Tab
P '\\' '"' -> Bool.true # Double quote
P '\\' '\\' -> Bool.true # Backslash
_ -> Bool.false
takeWhile = \list, predicate ->
helper = \{ taken, rest } ->
when List.first rest is
Ok elem ->
if predicate elem then
helper { taken: List.append taken elem, rest: List.split rest 1 |> .others }
when rest is
[a, b, ..] ->
if isEscapeSequence a b then
helper {
taken: taken |> List.append a |> List.append b,
rest: List.drop rest 2,
}
else if predicate a then
helper {
taken: List.append taken a,
rest: List.dropFirst rest,
}
else
{ taken, rest }
Err _ -> { taken, rest }
[a, ..] if predicate a ->
helper {
taken: List.append taken a,
rest: List.dropFirst rest,
}
_ -> { taken, rest }
helper { taken: [], rest: list }
@ -341,7 +430,6 @@ jsonString = \bytes ->
if
before == ['"']
then
# TODO: handle escape sequences
{ taken: strSequence, rest } = takeWhile afterStartingQuote \n -> n != '"'
when Str.fromUtf8 strSequence is
@ -358,42 +446,38 @@ decodeString = Decode.custom \bytes, @Json {} ->
jsonString bytes
decodeList = \decodeElem -> Decode.custom \bytes, @Json {} ->
decodeElems = \chunk, accum ->
when Decode.decodeWith chunk decodeElem (@Json {}) is
{ result, rest } ->
when result is
Ok val ->
# TODO: handle spaces before ','
{ before: afterElem, others } = List.split rest 1
if
afterElem == [',']
then
decodeElems others (List.append accum val)
else
Done (List.append accum val) rest
Err e -> Errored e rest
Ok val ->
restWithoutWhitespace = eatWhitespace rest
when restWithoutWhitespace is
[',', ..] -> decodeElems (eatWhitespace (List.dropFirst restWithoutWhitespace)) (List.append accum val)
_ -> Done (List.append accum val) restWithoutWhitespace
{ before, others: afterStartingBrace } = List.split bytes 1
when bytes is
['[', ']'] -> { result: Ok [], rest: List.drop bytes 2 }
['[', ..] ->
bytesWithoutWhitespace = eatWhitespace (List.dropFirst bytes)
when bytesWithoutWhitespace is
[']', ..] ->
{ result: Ok [], rest: List.dropFirst bytesWithoutWhitespace }
if
before == ['[']
then
# TODO: empty lists
when decodeElems afterStartingBrace [] is
Errored e rest -> { result: Err e, rest }
Done vals rest ->
{ before: maybeEndingBrace, others: afterEndingBrace } = List.split rest 1
_ ->
when decodeElems bytesWithoutWhitespace [] is
Errored e rest ->
{ result: Err e, rest }
if
maybeEndingBrace == [']']
then
{ result: Ok vals, rest: afterEndingBrace }
else
{ result: Err TooShort, rest }
else
{ result: Err TooShort, rest: bytes }
Done vals rest ->
when rest is
[']', ..] -> { result: Ok vals, rest: List.dropFirst rest }
_ -> { result: Err TooShort, rest }
_ ->
{ result: Err TooShort, rest: bytes }
parseExactChar : List U8, U8 -> DecodeResult {}
parseExactChar = \bytes, char ->
@ -414,6 +498,12 @@ openBrace = \bytes -> parseExactChar bytes '{'
closingBrace : List U8 -> DecodeResult {}
closingBrace = \bytes -> parseExactChar bytes '}'
openBracket : List U8 -> DecodeResult {}
openBracket = \bytes -> parseExactChar bytes '['
closingBracket : List U8 -> DecodeResult {}
closingBracket = \bytes -> parseExactChar bytes ']'
recordKey : List U8 -> DecodeResult Str
recordKey = \bytes -> jsonString bytes
@ -463,3 +553,92 @@ decodeRecord = \initialState, stepField, finalizer -> Decode.custom \bytes, @Jso
when finalizer endStateResult is
Ok val -> { result: Ok val, rest: afterRecordBytes }
Err e -> { result: Err e, rest: afterRecordBytes }
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 }
# Helper to eat leading Json whitespace characters
eatWhitespace = \input ->
when input is
[' ', ..] -> eatWhitespace (List.dropFirst input)
['\n', ..] -> eatWhitespace (List.dropFirst input)
['\r', ..] -> eatWhitespace (List.dropFirst input)
['\t', ..] -> eatWhitespace (List.dropFirst input)
_ -> input
# Test eating Json whitespace
expect
input = Str.toUtf8 " \n\r\tabc"
actual = eatWhitespace input
expected = Str.toUtf8 "abc"
actual == expected
# Test json string decoding with escapes
expect
input = Str.toUtf8 "\"a\r\nbc\\\"xz\""
expected = Ok "a\r\nbc\\\"xz"
actual = Decode.fromBytes input fromUtf8
actual == expected
# Test json string encoding with escapes
expect
input = "a\r\nbc\\\"xz"
expected = Str.toUtf8 "\"a\r\nbc\\\"xz\""
actual = Encode.toBytes input toUtf8
actual == expected
# Test json array decode empty list
expect
input = Str.toUtf8 "[ ]"
actual : Result (List U8) _
actual = Decode.fromBytes input fromUtf8
expected = Ok []
actual == expected
# Test json array decoding into integers
expect
input = Str.toUtf8 "[ 1,\n2,\t3]"
actual : Result (List U8) _
actual = Decode.fromBytes input fromUtf8
expected = Ok [1, 2, 3]
actual == expected
# Test json array decoding into strings ignoring whitespace around values
expect
input = Str.toUtf8 "[\r\"one\" ,\t\"two\"\n,\n\"3\"\t]"
actual = Decode.fromBytes input fromUtf8
expected = Ok ["one", "two", "3"]
actual == expected

View file

@ -62,6 +62,7 @@ interface List
sortAsc,
sortDesc,
reserve,
releaseExcessCapacity,
walkBackwardsUntil,
countIf,
]
@ -73,11 +74,11 @@ interface List
## 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
##
## ```
## [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)
@ -105,11 +106,11 @@ interface List
## will be immediately freed.
##
## Let's look at an example.
## ```
## ratings = [5, 4, 3]
##
## ratings = [5, 4, 3]
##
## { foo: ratings, bar: ratings }
##
## { 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.
##
@ -118,14 +119,14 @@ interface List
## refcount getting incremented from 1 to 3.
##
## Let's turn this example into a function.
## ```
## getRatings = \first ->
## ratings = [first, 4, 3]
##
## getRatings = \first ->
## ratings = [first, 4, 3]
##
## { foo: ratings, bar: ratings }
##
## getRatings 5
## { 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
@ -140,23 +141,23 @@ interface List
## 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]
##
## getRatings = \first ->
## ratings = [first, 4, 3]
##
## { foo: ratings, bar: ratings }
##
## (getRatings 5).bar
## { 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
##
## ```
## 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
@ -167,26 +168,26 @@ interface List
## 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]
##
## nums = [1, 2, 3, 4, 5, 6, 7]
##
## first = List.first nums
## last = List.last nums
##
## first
## 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]]
##
## lists = [[1], [2, 3], [], [4, 5, 6, 7]]
## first = List.first lists
## last = List.last lists
##
## 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,
## 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
@ -206,10 +207,11 @@ interface List
## * 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 [1, 2, 3]
##
## >>> List.isEmpty []
## List.isEmpty []
## ```
isEmpty : List a -> Bool
isEmpty = \list ->
List.len list == 0
@ -237,9 +239,9 @@ replace = \list, index, newValue ->
{ list, value: newValue }
## Replaces the element at the given index with a replacement.
##
## >>> List.set ["a", "b", "c"] 1 "B"
##
## ```
## List.set ["a", "b", "c"] 1 "B"
## ```
## If the given index is outside the bounds of the list, returns the original
## list unmodified.
##
@ -249,11 +251,12 @@ 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
##
## >>> List.append [1, 2, 3] 4
##
## >>> [0, 1, 2]
## >>> |> List.append 3
## [0, 1, 2]
## |> List.append 3
## ```
append : List a, a -> List a
append = \list, element ->
list
@ -268,11 +271,12 @@ append = \list, element ->
appendUnsafe : List a, a -> List a
## Add a single element to the beginning of a list.
## ```
## List.prepend [1, 2, 3] 0
##
## >>> List.prepend [1, 2, 3] 0
##
## >>> [2, 3, 4]
## >>> |> List.prepend 1
## [2, 3, 4]
## |> List.prepend 1
## ```
prepend : List a, a -> List a
## Returns the length of the list - the number of elements it contains.
@ -288,12 +292,17 @@ withCapacity : Nat -> List a
## 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]
##
## >>> List.concat [1, 2, 3] [4, 5]
##
## >>> [0, 1, 2]
## >>> |> List.concat [3, 4]
## [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.
@ -306,17 +315,15 @@ last = \list ->
## A list with a single element in it.
##
## This is useful in pipelines, like so:
##
## websites =
## Str.concat domain ".com"
## |> List.single
##
## ```
## 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)
@ -329,8 +336,9 @@ repeatHelp = \value, count, accum ->
accum
## Returns the list with its elements reversed.
##
## >>> List.reverse [1, 2, 3]
## ```
## List.reverse [1, 2, 3]
## ```
reverse : List a -> List a
reverse = \list ->
reverseHelp list 0 (Num.subSaturated (List.len list) 1)
@ -342,12 +350,11 @@ reverseHelp = \list, left, right ->
list
## Join the given lists together into one list.
##
## >>> List.join [[1, 2, 3], [4, 5], [], [6, 7]]
##
## >>> List.join [[], []]
##
## >>> List.join []
## ```
## List.join [[1, 2, 3], [4, 5], [], [6, 7]]
## List.join [[], []]
## List.join []
## ```
join : List (List a) -> List a
join = \lists ->
totalLength =
@ -366,10 +373,10 @@ contains = \list, needle ->
## 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
##
## ```
## [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`.
@ -385,10 +392,10 @@ contains = \list, needle ->
## 6 | 8 | 14
##
## The following returns -6:
##
## [1, 2, 3]
## |> List.walk 0 Num.sub
##
## ```
## [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
@ -494,9 +501,9 @@ all = \list, predicate ->
## 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)
##
## ```
## List.keepIf [1, 2, 3, 4] (\num -> num > 2)
## ```
## ## Performance Details
##
## [List.keepIf] always returns a list that takes up exactly the same amount
@ -531,9 +538,9 @@ keepIfHelp = \list, predicate, kept, index, length ->
## 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)
##
## ```
## List.dropIf [1, 2, 3, 4] (\num -> num > 2)
## ```
## ## Performance Details
##
## `List.dropIf` has the same performance characteristics as [List.keepIf].
@ -556,12 +563,13 @@ countIf = \list, predicate ->
## 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
##
## >>> List.keepOks [["a", "b"], [], [], ["c", "d", "e"]] List.last
## fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok (Str.len str)
##
## >>> fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok (Str.len str)
## >>>
## >>> List.keepOks ["", "a", "bc", "", "d", "ef", ""]
## List.keepOks ["", "a", "bc", "", "d", "ef", ""]
## ```
keepOks : List before, (before -> Result after *) -> List after
keepOks = \list, toResult ->
walker = \accum, element ->
@ -573,12 +581,13 @@ keepOks = \list, toResult ->
## 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
##
## >>> List.keepErrs [["a", "b"], [], [], ["c", "d", "e"]] List.last
## fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok (Str.len str)
##
## >>> fn = \str -> if Str.isEmpty str then Err StrWasEmpty else Ok (Str.len str)
## >>>
## >>> List.keepErrs ["", "a", "bc", "", "d", "ef", ""]
## List.keepErrs ["", "a", "bc", "", "d", "ef", ""]
## ```
keepErrs : List before, (before -> Result * after) -> List after
keepErrs = \list, toResult ->
walker = \accum, element ->
@ -590,10 +599,11 @@ keepErrs = \list, toResult ->
## 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 [1, 2, 3] (\num -> num + 1)
##
## > List.map ["", "a", "bc"] Str.isEmpty
## List.map ["", "a", "bc"] Str.isEmpty
## ```
map : List a, (a -> b) -> List b
## Run a transformation function on the first element of each list,
@ -602,8 +612,9 @@ map : List a, (a -> b) -> List b
##
## 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
## ```
## 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,
@ -640,92 +651,110 @@ mapWithIndexHelp = \src, dest, func, index, length ->
## 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]
##
## ```
## 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]
##
## ```
## 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]
##
## ```
## 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 { 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 } ->
{ incByStep, stepIsPositive } =
{ 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
{
incByStep: \i -> i + 1,
calcNext: \i -> Num.addChecked i 1,
stepIsPositive: Bool.true,
}
else
{
incByStep: \i -> i - 1,
calcNext: \i -> Num.subChecked i 1,
stepIsPositive: Bool.false,
}
T (At _) (Length _) | T (After _) (Length _) ->
{
incByStep: \i -> i + 1,
calcNext: \i -> Num.addChecked i 1,
stepIsPositive: Bool.true,
}
else
{
incByStep: \i -> i + step,
calcNext: \i -> Num.addChecked i step,
stepIsPositive: step > 0,
}
inclusiveStart =
when start is
At x -> x
After x -> incByStep x
At x -> Ok x
After x -> calcNext x
when end is
At at ->
isComplete =
isValid =
if stepIsPositive then
\i -> i > at
\i -> i <= at
else
\i -> i < at
\i -> i >= at
# TODO: switch to List.withCapacity
rangeHelp [] inclusiveStart incByStep isComplete
rangeHelp [] inclusiveStart calcNext isValid
Before before ->
isComplete =
isValid =
if stepIsPositive then
\i -> i >= before
\i -> i < before
else
\i -> i <= before
\i -> i > before
# TODO: switch to List.withCapacity
rangeHelp [] inclusiveStart incByStep isComplete
rangeHelp [] inclusiveStart calcNext isValid
Length l ->
rangeLengthHelp (List.withCapacity l) inclusiveStart l incByStep
rangeLengthHelp (List.withCapacity l) inclusiveStart l calcNext
rangeHelp = \accum, i, incByStep, isComplete ->
if isComplete i then
accum
else
# TODO: change this to List.appendUnsafe once capacity is set correctly
rangeHelp (List.append accum i) (incByStep i) incByStep isComplete
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
rangeLengthHelp = \accum, i, remaining, incByStep ->
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
rangeLengthHelp (List.appendUnsafe accum i) (incByStep i) (remaining - 1) incByStep
when i is
Ok val ->
rangeLengthHelp (List.appendUnsafe accum val) (calcNext val) (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]
@ -754,6 +783,18 @@ expect
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
@ -795,14 +836,14 @@ dropLast = \list ->
List.dropAt list (Num.subSaturated (List.len list) 1)
## Returns the given number of elements from the beginning of the list.
##
## >>> List.takeFirst [1, 2, 3, 4, 5, 6, 7, 8] 4
##
## ```
## 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
##
## ```
## 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,
@ -810,28 +851,19 @@ dropLast = \list ->
##
## 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 list 1` 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
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
##
## ```
## 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
##
## ```
## 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,
@ -839,16 +871,6 @@ takeFirst = \list, outputLength ->
##
## 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 list 1` 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
takeLast = \list, outputLength ->
List.sublist list { start: Num.subSaturated (List.len list) outputLength, len: outputLength }
@ -971,13 +993,13 @@ findLastIndex = \list, matches ->
## 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 }
##
## ```
## 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 }
##
## ```
## 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.
##
@ -993,7 +1015,9 @@ sublist = \list, config ->
sublistLowlevel : List elem, Nat, Nat -> List elem
## Intersperses `sep` between the elements of `list`
## >>> List.intersperse 9 [1, 2, 3] # [1, 9, 2, 9, 3]
## ```
## List.intersperse 9 [1, 2, 3] # [1, 9, 2, 9, 3]
## ```
intersperse : List elem, elem -> List elem
intersperse = \list, sep ->
capacity = 2 * List.len list
@ -1051,8 +1075,9 @@ split = \elements, userSplitIndex ->
## 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, Baz] }
## ```
## 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] | elem has Eq
splitFirst = \list, delimiter ->
when List.findFirstIndex list (\elem -> elem == delimiter) is
@ -1066,8 +1091,9 @@ splitFirst = \list, delimiter ->
## 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, Bar], after: [Baz] }
## ```
## 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] | elem has Eq
splitLast = \list, delimiter ->
when List.findLastIndex list (\elem -> elem == delimiter) is

View file

@ -68,6 +68,9 @@ interface Num
compare,
pow,
powInt,
countLeadingZeroBits,
countTrailingZeroBits,
countOneBits,
addWrap,
addChecked,
addSaturated,
@ -86,6 +89,8 @@ interface Num
intCast,
bytesToU16,
bytesToU32,
bytesToU64,
bytesToU128,
divCeil,
divCeilChecked,
divTrunc,
@ -151,9 +156,9 @@ interface Num
## 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
##
## ```
## 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 *)`.
##
@ -191,9 +196,9 @@ interface Num
##
## 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
##
## ```
## 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:
@ -234,7 +239,7 @@ Num range := range
##
## This pattern continues up to [U128] and [I128].
##
## ## Performance notes
## ## 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
@ -259,15 +264,11 @@ Num range := range
##
## 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
##
## ```
## 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.
@ -342,16 +343,16 @@ Int range : Num (Integer range)
##
## 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
##
## ```
## 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
##
## ```
## 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.
@ -380,7 +381,7 @@ Int range : Num (Integer range)
## Whenever a function in this module could return one of these values, that
## possibility is noted in the function's documentation.
##
## ## Performance Notes
## ## 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
@ -480,7 +481,7 @@ F32 : Num (FloatingPoint Binary32)
## 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
## ## Performance Details
##
## [Dec] typically takes slightly less time than [F64] to perform addition and
## subtraction, but 10-20 times longer to perform multiplication and division.
@ -492,15 +493,14 @@ Dec : Num (FloatingPoint Decimal)
##
## This is the same as calling `Num.format {}` - so for more details on
## exact formatting, see `Num.format`.
##
## >>> Num.toStr 42
##
## ```
## 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
##
## ```
## 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 `"-∞"`.
##
@ -510,6 +510,8 @@ 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 ->
@ -531,6 +533,26 @@ bytesToU32 = \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 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 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.
@ -539,9 +561,10 @@ compare : Num a, Num a -> [LT, EQ, GT]
##
## 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
## ```
## 5
## |> Num.isLt 6
## ```
isLt : Num a, Num a -> Bool
## Returns `Bool.true` if the first number is greater than the second.
@ -550,9 +573,10 @@ isLt : Num a, Num a -> Bool
##
## 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
## ```
## 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.
@ -601,15 +625,15 @@ toFrac : Num * -> Frac *
## * 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 4
## Num.abs -2.5
##
## >>> Num.abs -2.5
##
## >>> Num.abs 0
##
## >>> Num.abs 0.0
## 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.
@ -620,15 +644,15 @@ toFrac : Num * -> Frac *
abs : Num a -> Num a
## Return a negative number when given a positive one, and vice versa.
## ```
## Num.neg 5
##
## >>> Num.neg 5
## Num.neg -2.5
##
## >>> Num.neg -2.5
##
## >>> Num.neg 0
##
## >>> Num.neg 0.0
## 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.
@ -645,16 +669,16 @@ neg : Num a -> Num a
## (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
##
## >>> 5 + 7
##
## >>> Num.add 5 7
##
## Num.add 5 7
## ```
## `Num.add` can be convenient in pipelines.
##
## >>> Frac.pi
## >>> |> Num.add 1.0
##
## ```
## 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
@ -666,16 +690,16 @@ add : Num a, Num a -> Num a
## (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
##
## >>> 7 - 5
##
## >>> Num.sub 7 5
##
## Num.sub 7 5
## ```
## `Num.sub` can be convenient in pipelines.
##
## >>> Frac.pi
## >>> |> Num.sub 2.0
##
## ```
## 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
@ -687,16 +711,18 @@ sub : Num a, Num a -> Num a
## (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
##
## >>> 5 * 7
##
## >>> Num.mul 5 7
## Num.mul 5 7
## ```
##
## `Num.mul` can be convenient in pipelines.
##
## >>> Frac.pi
## >>> |> Num.mul 2.0
##
## ```
## 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
@ -731,14 +757,15 @@ atan : Frac a -> Frac a
## > 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 4.0
## Num.sqrt 1.5
##
## >>> Num.sqrt 1.5
## Num.sqrt 0.0
##
## >>> Num.sqrt 0.0
##
## >>> Num.sqrt -4.0f64
## Num.sqrt -4.0f64
## ```
sqrt : Frac a -> Frac a
sqrtChecked : Frac a -> Result (Frac a) [SqrtOfNegative]
@ -748,6 +775,7 @@ sqrtChecked = \x ->
else
Ok (Num.sqrt x)
## Natural logarithm
log : Frac a -> Frac a
logChecked : Frac a -> Result (Frac a) [LogNeedsPositive]
@ -778,15 +806,16 @@ logChecked = \x ->
##
## 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
##
## >>> 5.0 / 7.0
##
## >>> Num.div 5 7
##
## Num.div 5 7
## ```
## `Num.div` can be convenient in pipelines.
##
## >>> Num.pi
## >>> |> Num.div 2.0
## ```
## Num.pi
## |> Num.div 2.0
## ```
div : Frac a, Frac a -> Frac a
divChecked : Frac a, Frac a -> Result (Frac a) [DivByZero]
@ -812,15 +841,15 @@ divCeilChecked = \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
##
## >>> 5 // 7
## Num.divTrunc 5 7
##
## >>> Num.divTrunc 5 7
##
## >>> 8 // -3
##
## >>> Num.divTrunc 8 -3
## 8 // -3
##
## Num.divTrunc 8 -3
## ```
divTrunc : Int a, Int a -> Int a
divTruncChecked : Int a, Int a -> Result (Int a) [DivByZero]
@ -833,14 +862,15 @@ divTruncChecked = \a, b ->
## Obtain the remainder (truncating modulo) from the division of two integers.
##
## `a % b` is shorthand for `Num.rem a b`.
## ```
## 5 % 7
##
## >>> 5 % 7
## Num.rem 5 7
##
## >>> Num.rem 5 7
## -8 % -3
##
## >>> -8 % -3
##
## >>> Num.rem -8 -3
## Num.rem -8 -3
## ```
rem : Int a, Int a -> Int a
remChecked : Int a, Int a -> Result (Int a) [DivByZero]
@ -860,24 +890,24 @@ bitwiseOr : Int a, Int a -> Int a
##
## 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
##
## >>> shiftLeftBy 0b0000_0011 2 == 0b0000_1100
##
## >>> 0b0000_0101 |> shiftLeftBy 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
##
## >>> shiftRightBy 0b0000_0011 2 == 0b0000_1100
##
## >>> 0b0001_0100 |> shiftRightBy 2 == 0b0000_0101
##
## >>> 0b1001_0000 |> shiftRightBy 2 == 0b1110_0100
## 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
@ -885,13 +915,13 @@ shiftRightBy : Int a, U8 -> Int a
##
## 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
##
## >>> shiftRightBy 0b0010_1000 2 == 0b0000_1010
##
## >>> 0b0010_1000 |> shiftRightBy 2 == 0b0000_1010
##
## >>> 0b1001_0000 |> shiftRightBy 2 == 0b0010_0100
## 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
@ -912,21 +942,60 @@ pow : Frac a, Frac a -> Frac a
##
## For a [Frac] alternative to this function, which supports negative exponents,
## see #Num.exp.
## ```
## Num.exp 5 0
##
## >>> Num.exp 5 0
## Num.exp 5 1
##
## >>> Num.exp 5 1
## Num.exp 5 2
##
## >>> Num.exp 5 2
##
## >>> Num.exp 5 6
##
## ## Performance Notes
## Num.exp 5 6
## ```
## ## Performance Details
##
## 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
## 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
## Add two numbers, clamping on the maximum representable number rather than
@ -1261,164 +1330,3 @@ 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 `Bool.false` if that value is
## [*NaN*](Num.isNaN), ∞ or -∞, and `Bool.true` otherwise.
##
## Always returns `Bool.true` when given a [Dec].
##
## This is the opposite of #isInfinite, except when given [*NaN*](Num.isNaN). Both
## #isFinite and #isInfinite return `Bool.false` for [*NaN*](Num.isNaN).
# isFinite : Frac * -> Bool
## When given a [F64] or [F32] value, returns `Bool.true` if that value is either
## ∞ or -∞, and `Bool.false` otherwise.
##
## Always returns `Bool.false` when given a [Dec].
##
## This is the opposite of #isFinite, except when given [*NaN*](Num.isNaN). Both
## #isFinite and #isInfinite return `Bool.false` for [*NaN*](Num.isNaN).
# isInfinite : Frac * -> Bool
## When given a [F64] or [F32] value, returns `Bool.true` if that value is
## *NaN* ([not a number](https://en.wikipedia.org/wiki/NaN)), and `Bool.false` otherwise.
##
## Always returns `Bool.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 `Bool.false` if either argument is *NaN*.
## * *NaN* has no ordering, so [isLt], [isLte], [isGt], and [isGte] always return `Bool.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.insert] and [Dict.insert] for details.
# isNaN : Frac * -> Bool
## Returns the higher of two numbers.
##
## 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).)
# max : Num a, Num a -> Num a
## Returns the lower of two numbers.
##
## 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).)
# 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

View file

@ -7,8 +7,9 @@ interface Result
Result ok err : [Ok ok, Err err]
## Return `Bool.true` if the result indicates a success, else return `Bool.false`
##
## >>> Result.isOk (Ok 5)
## ```
## Result.isOk (Ok 5)
## ```
isOk : Result ok err -> Bool
isOk = \result ->
when result is
@ -16,8 +17,9 @@ isOk = \result ->
Err _ -> Bool.false
## Return `Bool.true` if the result indicates a failure, else return `Bool.false`
##
## >>> Result.isErr (Err "uh oh")
## ```
## Result.isErr (Err "uh oh")
## ```
isErr : Result ok err -> Bool
isErr = \result ->
when result is
@ -26,10 +28,10 @@ isErr = \result ->
## 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
## ```
## Result.withDefault (Ok 7) 42
## Result.withDefault (Err "uh oh") 42
## ```
withDefault : Result ok err, ok -> ok
withDefault = \result, default ->
when result is
@ -37,16 +39,15 @@ withDefault = \result, default ->
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.
## 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
## ```
##
## (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`.
## 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
@ -54,13 +55,12 @@ map = \result, transform ->
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
## 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
@ -68,13 +68,12 @@ mapErr = \result, transform ->
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 `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
## function on the value the `Ok` holds. Then return 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
@ -82,13 +81,12 @@ try = \result, transform ->
Err e -> Err e
## If the result is `Err`, transform the entire result by running a conversion
## function on the value the `Err` holds. Then return 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
## function on the value the `Err` holds. Then return 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

View file

@ -4,6 +4,7 @@ interface Set
empty,
single,
walk,
walkUntil,
insert,
len,
remove,
@ -25,6 +26,8 @@ interface Set
# We should have this line above the next has.
# It causes the formatter to fail currently.
# | k has Hash & Eq
## 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 {}
has [
Eq {
@ -43,14 +46,39 @@ isEq = \xs, ys ->
else
Break Bool.false
## Creates a new empty set.
## Creates a new empty `Set`.
## ```
## emptySet = Set.empty {}
## countValues = Set.len emptySet
##
## expect countValues == 0
## ```
empty : {} -> Set k | k has Hash & Eq
empty = \{} -> @Set (Dict.empty {})
## Creates a new `Set` with a single value.
## ```
## singleItemSet = Set.single "Apple"
## countValues = Set.len singleItemSet
##
## expect countValues == 1
## ```
single : k -> Set k | k has 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 | k has Hash & Eq
insert = \@Set dict, key ->
Dict.insert dict key {} |> @Set
@ -72,6 +100,18 @@ expect
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 k -> Nat | k has Hash & Eq
len = \@Set dict ->
Dict.len dict
@ -88,41 +128,151 @@ expect
actual == 3
## Drops the given element from the set.
## 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 | k has 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 | k has 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 | k has 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 | k has 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 | k has 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 | k has 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 | k has 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 | k has Hash & Eq
walk = \@Set dict, state, step ->
Dict.walk dict state (\s, k, _ -> step s 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 | k has Hash & Eq
walkUntil = \@Set dict, state, step ->
Dict.walkUntil dict state (\s, k, _ -> step s k)

View file

@ -1,14 +1,13 @@
## Working with Unicode strings in Roc.
##
## ### Unicode
## ## 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 "👩‍👩‍👦‍👦".
@ -17,11 +16,11 @@
## 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 "🕊"
##
## ```
## 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`.
@ -31,23 +30,23 @@
## 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."
##
## ```
## "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`.
## 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.
## `0x0A` is a Roc hexadecimal literal, and `\u` escape sequences are always
## followed by a hexadecimal literal inside `{` and `}` like this.
## `\u` escape sequences are always followed by a hexadecimal number 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
## 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{0xF6}c"` as an alternative way to get the string `"Röc"\.
## you can write `"R\u(F6)c"` as an alternative way to get the string `"Röc"\.
##
## Roc strings also support these escape sequences:
##
@ -58,12 +57,11 @@
## * `\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)."
##
## ```
## 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
@ -111,6 +109,7 @@ interface Str
splitLast,
walkUtf8WithIndex,
reserve,
releaseExcessCapacity,
appendScalar,
walkScalars,
walkScalarsUntil,
@ -125,7 +124,6 @@ interface Str
Num.{ Nat, Num, U8, U16, U32, U64, U128, I8, I16, I32, I64, I128, F32, F64, Dec },
]
## Test
Utf8ByteProblem : [
InvalidStartByte,
UnexpectedEndOfSequence,
@ -138,16 +136,18 @@ Utf8ByteProblem : [
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
## ```
## 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 "" "" == ""
## ```
## 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.
@ -155,9 +155,10 @@ withCapacity : 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"
## ```
## 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.
@ -165,20 +166,22 @@ joinWith : List Str, Str -> Str
## 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"]
## ```
## 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"
##
## ```
## 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 == ""
## ```
## 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)
@ -186,11 +189,11 @@ repeat : Str, Nat -> Str
##
## 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
##
## ```
## 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
@ -205,12 +208,15 @@ graphemes : Str -> List Str
##
## 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
## ```
##
## 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
##
## **Performance Note:** This runs slightly faster than [Str.startsWith], so
## 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`.)
@ -225,36 +231,39 @@ startsWithScalar : Str, U32 -> Bool
##
## (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 "" == []
## ```
## 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]
## ```
## 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
## ```
## 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)
@ -266,8 +275,9 @@ fromUtf8 = \bytes ->
## 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"
## ```
## 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 config.start + config.count <= List.len bytes then
@ -290,57 +300,65 @@ FromUtf8Result : {
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
## ```
## 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
## ```
## 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"
## ```
## expect Str.trim " Hello \n\n" == "Hello"
## ```
trim : Str -> Str
## Return the [Str] with all whitespace removed from the beginning.
##
## expect Str.trimLeft " Hello \n\n" == "Hello \n\n"
## ```
## expect Str.trimLeft " Hello \n\n" == "Hello \n\n"
## ```
trimLeft : Str -> Str
## Return the [Str] with all whitespace removed from the end.
##
## expect Str.trimRight " Hello \n\n" == " Hello"
## ```
## expect Str.trimRight " Hello \n\n" == " Hello"
## ```
trimRight : 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
## ```
## 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
## ```
## 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
## ```
## 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
@ -356,20 +374,22 @@ toF32 = \string -> strToNumHelp string
## 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
## ```
## 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
## ```
## 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
@ -377,96 +397,105 @@ toU128 = \string -> strToNumHelp string
## 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
## ```
## 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
## ```
## 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
## ```
## 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
## ```
## 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
## ```
## 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
## ```
## 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
## ```
## 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
## ```
## 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
## ```
## 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
@ -474,8 +503,9 @@ toI8 = \string -> strToNumHelp string
getUnsafe : Str, Nat -> U8
## Gives the number of bytes in a [Str] value.
##
## expect Str.countUtf8Bytes "Hello World" == 11
## ```
## expect Str.countUtf8Bytes "Hello World" == 11
## ```
countUtf8Bytes : Str -> Nat
## string slice that does not do bounds checking or utf-8 verification
@ -483,9 +513,10 @@ substringUnsafe : Str, Nat, Nat -> Str
## Returns the given [Str] with each occurrence of a substring replaced.
## Returns [Err NotFound] if the substring is not found.
##
## expect Str.replaceEach "foo/bar/baz" "/" "_" == Ok "foo_bar_baz"
## expect Str.replaceEach "not here" "/" "_" == Err NotFound
## ```
## expect Str.replaceEach "foo/bar/baz" "/" "_" == Ok "foo_bar_baz"
## expect Str.replaceEach "not here" "/" "_" == Err NotFound
## ```
replaceEach : Str, Str, Str -> Result Str [NotFound]
replaceEach = \haystack, needle, flower ->
when splitFirst haystack needle is
@ -515,9 +546,10 @@ expect Str.replaceEach "abXdeXghi" "X" "_" == Ok "ab_de_ghi"
## Returns the given [Str] with the first occurrence of a substring replaced.
## Returns [Err NotFound] if the substring is not found.
##
## expect Str.replaceFirst "foo/bar/baz" "/" "_" == Ok "foo_bar/baz"
## expect Str.replaceFirst "no slashes here" "/" "_" == Err NotFound
## ```
## expect Str.replaceFirst "foo/bar/baz" "/" "_" == Ok "foo_bar/baz"
## expect Str.replaceFirst "no slashes here" "/" "_" == Err NotFound
## ```
replaceFirst : Str, Str, Str -> Result Str [NotFound]
replaceFirst = \haystack, needle, flower ->
when splitFirst haystack needle is
@ -530,9 +562,10 @@ expect Str.replaceFirst "abXdeXghi" "X" "_" == Ok "ab_deXghi"
## Returns the given [Str] with the last occurrence of a substring replaced.
## Returns [Err NotFound] if the substring is not found.
##
## expect Str.replaceLast "foo/bar/baz" "/" "_" == Ok "foo/bar_baz"
## expect Str.replaceLast "no slashes here" "/" "_" == Err NotFound
## ```
## expect Str.replaceLast "foo/bar/baz" "/" "_" == Ok "foo/bar_baz"
## expect Str.replaceLast "no slashes here" "/" "_" == Err NotFound
## ```
replaceLast : Str, Str, Str -> Result Str [NotFound]
replaceLast = \haystack, needle, flower ->
when splitLast haystack needle is
@ -546,9 +579,10 @@ expect Str.replaceLast "abXdeXghi" "X" "_" == Ok "abXde_ghi"
## 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
## ```
## 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
@ -599,9 +633,10 @@ firstMatchHelp = \haystack, needle, index, lastPossible ->
## 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
## ```
## 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
@ -690,10 +725,11 @@ matchesAtHelp = \state ->
## 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]
## ```
## 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)
@ -711,14 +747,19 @@ walkUtf8WithIndexHelp = \string, state, step, index, length ->
## Enlarge a string for at least the given number additional bytes.
reserve : Str, Nat -> Str
## Shrink the memory footprint of a str such that it's 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
## ```
## 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
@ -734,10 +775,11 @@ 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]
## ```
## 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)
@ -754,16 +796,17 @@ walkScalarsHelp = \string, state, step, index, length ->
## 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]
## ```
## 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)
@ -795,7 +838,8 @@ strToNumHelp = \string ->
Err InvalidNumStr
## Adds a prefix to the given [Str].
##
## expect Str.withPrefix "Awesome" "Roc" == "RocAwesome"
## ```
## expect Str.withPrefix "Awesome" "Roc" == "RocAwesome"
## ```
withPrefix : Str, Str -> Str
withPrefix = \str, prefix -> Str.concat prefix str

View file

@ -1,73 +1,6 @@
use roc_module::symbol::Symbol;
use roc_target::TargetInfo;
use std::ops::Index;
use tempfile::NamedTempFile;
pub const HOST_WASM: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/bitcode/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)]
pub const HOST_UNIX: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/bitcode/builtins-host.o"));
#[cfg(windows)]
pub const HOST_WINDOWS: &[u8] = include_bytes!(concat!(
env!("OUT_DIR"),
"/bitcode/builtins-windows-x86_64.obj"
));
pub fn host_wasm_tempfile() -> std::io::Result<NamedTempFile> {
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<NamedTempFile> {
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<NamedTempFile> {
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<NamedTempFile> {
#[cfg(unix)]
{
host_unix_tempfile()
}
#[cfg(windows)]
{
host_windows_tempfile()
}
#[cfg(not(any(windows, unix)))]
{
unreachable!()
}
}
#[derive(Debug, Default, Copy, Clone)]
pub struct IntrinsicName {
@ -356,8 +289,16 @@ pub const NUM_MUL_CHECKED_INT: IntrinsicName = int_intrinsic!("roc_builtins.num.
pub const NUM_MUL_CHECKED_FLOAT: IntrinsicName =
float_intrinsic!("roc_builtins.num.mul_with_overflow");
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";
@ -392,6 +333,8 @@ 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";
@ -409,6 +352,9 @@ 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_FROM_STR: &str = "roc_builtins.dec.from_str";
pub const DEC_TO_STR: &str = "roc_builtins.dec.to_str";

View file

@ -1,28 +1,29 @@
[package]
name = "roc_can"
version = "0.0.1"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
edition = "2021"
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_region = { path = "../region" }
roc_module = { path = "../module" }
roc_parse = { path = "../parse" }
roc_problem = { path = "../problem" }
roc_types = { path = "../types" }
roc_region = { path = "../region" }
roc_serialize = { path = "../serialize" }
bumpalo.workspace = true
static_assertions.workspace = true
bitvec.workspace = true
roc_types = { path = "../types" }
ven_pretty = { path = "../../vendor/pretty" }
bitvec.workspace = true
bumpalo.workspace = true
static_assertions.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true
indoc.workspace = true
pretty_assertions.workspace = true

View file

@ -448,7 +448,7 @@ pub fn find_type_def_symbols(
As(actual, _, _) => {
stack.push(&actual.value);
}
Tuple { fields: _, ext: _ } => {
Tuple { elems: _, ext: _ } => {
todo!("find_type_def_symbols: Tuple");
}
Record { fields, ext } => {
@ -872,8 +872,41 @@ fn can_annotation_help(
}
}
Tuple { fields: _, ext: _ } => {
todo!("tuple");
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(
@ -1440,6 +1473,39 @@ fn can_assigned_fields<'a>(
field_types
}
// TODO trim down these arguments!
#[allow(clippy::too_many_arguments)]
fn can_assigned_tuple_elems<'a>(
env: &mut Env,
pol: CanPolarity,
elems: &&[Loc<TypeAnnotation<'a>>],
scope: &mut Scope,
var_store: &mut VarStore,
introduced_variables: &mut IntroducedVariables,
local_aliases: &mut VecMap<Symbol, Alias>,
references: &mut VecSet<Symbol>,
) -> VecMap<usize, Type> {
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>(

View file

@ -126,6 +126,7 @@ map_symbol_to_lowlevel_and_arity! {
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,
@ -145,6 +146,7 @@ map_symbol_to_lowlevel_and_arity! {
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,
@ -187,6 +189,8 @@ map_symbol_to_lowlevel_and_arity! {
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,
@ -194,6 +198,9 @@ map_symbol_to_lowlevel_and_arity! {
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,
Eq; BOOL_STRUCTURAL_EQ; 2,
NotEq; BOOL_STRUCTURAL_NOT_EQ; 2,

View file

@ -1,10 +1,9 @@
use crate::{
def::Def,
expr::{
ClosureData, Expr, Field, OpaqueWrapFunctionData, RecordAccessorData, TupleAccessorData,
WhenBranchPattern,
ClosureData, Expr, Field, OpaqueWrapFunctionData, StructAccessorData, WhenBranchPattern,
},
pattern::{DestructType, ListPatterns, Pattern, RecordDestruct},
pattern::{DestructType, ListPatterns, Pattern, RecordDestruct, TupleDestruct},
};
use roc_module::{
ident::{Lowercase, TagName},
@ -513,7 +512,7 @@ fn deep_copy_expr_help<C: CopyEnv>(env: &mut C, copied: &mut Vec<Variable>, expr
field: field.clone(),
},
RecordAccessor(RecordAccessorData {
RecordAccessor(StructAccessorData {
name,
function_var,
record_var,
@ -521,7 +520,7 @@ fn deep_copy_expr_help<C: CopyEnv>(env: &mut C, copied: &mut Vec<Variable>, expr
ext_var,
field_var,
field,
}) => RecordAccessor(RecordAccessorData {
}) => RecordAccessor(StructAccessorData {
name: *name,
function_var: sub!(*function_var),
record_var: sub!(*record_var),
@ -545,24 +544,6 @@ fn deep_copy_expr_help<C: CopyEnv>(env: &mut C, copied: &mut Vec<Variable>, expr
index: *index,
},
TupleAccessor(TupleAccessorData {
name,
function_var,
tuple_var: record_var,
closure_var,
ext_var,
elem_var: field_var,
index,
}) => TupleAccessor(TupleAccessorData {
name: *name,
function_var: sub!(*function_var),
tuple_var: sub!(*record_var),
closure_var: sub!(*closure_var),
ext_var: sub!(*ext_var),
elem_var: sub!(*field_var),
index: *index,
}),
RecordUpdate {
record_var,
ext_var,
@ -794,6 +775,30 @@ fn deep_copy_pattern_help<C: CopyEnv>(
})
.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,

View file

@ -5,7 +5,7 @@ use crate::expr::Expr::{self, *};
use crate::expr::{
ClosureData, DeclarationTag, Declarations, FunctionDef, OpaqueWrapFunctionData, WhenBranch,
};
use crate::pattern::{Pattern, RecordDestruct};
use crate::pattern::{Pattern, RecordDestruct, TupleDestruct};
use roc_module::symbol::{Interns, ModuleId, Symbol};
@ -35,7 +35,10 @@ pub fn pretty_print_declarations(c: &Ctx, declarations: &Declarations) -> String
DeclarationTag::Expectation => todo!(),
DeclarationTag::ExpectationFx => todo!(),
DeclarationTag::Destructure(_) => todo!(),
DeclarationTag::MutualRecursion { .. } => todo!(),
DeclarationTag::MutualRecursion { .. } => {
// the defs will be printed next
continue;
}
};
defs.push(def);
@ -123,9 +126,10 @@ fn toplevel_function<'a>(
.append(f.line())
.append(f.text("\\"))
.append(f.intersperse(args, f.text(", ")))
.append(f.text("->"))
.append(f.text(" ->"))
.group()
.append(f.line())
.append(expr(c, EPrec::Free, f, body))
.append(expr(c, EPrec::Free, f, body).group())
.nest(2)
.group()
}
@ -330,12 +334,15 @@ fn expr<'a>(c: &Ctx, p: EPrec, f: &'a Arena<'a>, e: &'a Expr) -> DocBuilder<'a,
} => expr(c, AppArg, f, &loc_expr.value)
.append(f.text(format!(".{}", field.as_str())))
.group(),
TupleAccess { .. } => todo!(),
TupleAccess {
loc_expr, index, ..
} => expr(c, AppArg, f, &loc_expr.value)
.append(f.text(format!(".{index}")))
.group(),
OpaqueWrapFunction(OpaqueWrapFunctionData { opaque_name, .. }) => {
f.text(format!("@{}", opaque_name.as_str(c.interns)))
}
RecordAccessor(_) => todo!(),
TupleAccessor(_) => todo!(),
RecordUpdate {
symbol, updates, ..
} => f
@ -386,7 +393,15 @@ fn expr<'a>(c: &Ctx, p: EPrec, f: &'a Arena<'a>, e: &'a Expr) -> DocBuilder<'a,
),
Crash { .. } => todo!(),
ZeroArgumentTag { .. } => todo!(),
OpaqueRef { .. } => 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!(),
@ -505,6 +520,19 @@ fn pattern<'a>(
)
.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)

View file

@ -15,7 +15,7 @@ use crate::expr::AnnotatedMark;
use crate::expr::ClosureData;
use crate::expr::Declarations;
use crate::expr::Expr::{self, *};
use crate::expr::RecordAccessorData;
use crate::expr::StructAccessorData;
use crate::expr::{canonicalize_expr, Output, Recursive};
use crate::pattern::{canonicalize_def_header_pattern, BindingsFromPattern, Pattern};
use crate::procedure::References;
@ -36,6 +36,7 @@ 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};
@ -45,6 +46,7 @@ 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;
@ -1995,6 +1997,16 @@ fn pattern_to_vars_by_symbol(
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);
@ -2316,19 +2328,23 @@ fn canonicalize_pending_body<'a>(
ident: defined_symbol,
..
},
ast::Expr::RecordAccessorFunction(field),
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(RecordAccessorData {
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: (*field).into(),
field,
}),
),
Output::default(),

View file

@ -10,9 +10,10 @@ 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, Subs, SubsFmtContent, Variable,
Content, FlatType, GetSubsSlice, RedundantMark, SortedTagsIterator, Subs, SubsFmtContent,
Variable,
};
use roc_types::types::AliasKind;
use roc_types::types::{gather_tags_unsorted_iter, AliasKind};
pub use roc_exhaustive::Context as ExhaustiveContext;
@ -80,6 +81,8 @@ enum IndexCtor<'a> {
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,
@ -112,6 +115,7 @@ impl<'a> IndexCtor<'a> {
}
RenderAs::Opaque => Self::Opaque,
RenderAs::Record(fields) => Self::Record(fields),
RenderAs::Tuple => Self::Tuple,
RenderAs::Guard => Self::Guard,
}
}
@ -145,9 +149,7 @@ fn index_var(
var = *structure;
}
Content::Structure(structure) => match structure {
FlatType::Func(_, _, _) | FlatType::FunctionOrTagUnion(_, _, _) => {
return Err(TypeError)
}
FlatType::Func(_, _, _) => return Err(TypeError),
FlatType::Apply(Symbol::LIST_LIST, args) => {
match (subs.get_subs_slice(*args), ctor) {
([elem_var], IndexCtor::List) => {
@ -208,6 +210,19 @@ fn index_var(
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
@ -354,6 +369,30 @@ fn sketch_pattern(pattern: &crate::pattern::Pattern) -> SketchedPattern {
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: _,
@ -616,64 +655,80 @@ fn convert_tag(subs: &Subs, whole_var: Variable, this_tag: &TagName) -> (Union,
use {Content::*, FlatType::*};
match dealias_tag(subs, content) {
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);
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.into_iter());
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(),
(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(), &[]));
}
let union = Union {
alternatives,
render_as: RenderAs::Tag,
};
(union, my_tag_id)
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.into_iter());
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 {

View file

@ -19,12 +19,13 @@ use roc_module::ident::{ForeignSymbol, Lowercase, TagName};
use roc_module::low_level::LowLevel;
use roc_module::symbol::Symbol;
use roc_parse::ast::{self, Defs, 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, LambdaSet, OptAbleVar, Type};
use roc_types::types::{Alias, Category, IndexOrField, LambdaSet, OptAbleVar, Type};
use std::fmt::{Debug, Display};
use std::{char, u32};
@ -186,8 +187,8 @@ pub enum Expr {
field: Lowercase,
},
/// field accessor as a function, e.g. (.foo) expr
RecordAccessor(RecordAccessorData),
/// tuple or field accessor as a function, e.g. (.foo) expr or (.1) expr
RecordAccessor(StructAccessorData),
TupleAccess {
tuple_var: Variable,
@ -197,9 +198,6 @@ pub enum Expr {
index: usize,
},
/// tuple accessor as a function, e.g. (.1) expr
TupleAccessor(TupleAccessorData),
RecordUpdate {
record_var: Variable,
ext_var: Variable,
@ -315,9 +313,8 @@ impl Expr {
Self::Record { .. } => Category::Record,
Self::EmptyRecord => Category::Record,
Self::RecordAccess { field, .. } => Category::RecordAccess(field.clone()),
Self::RecordAccessor(data) => Category::RecordAccessor(data.field.clone()),
Self::RecordAccessor(data) => Category::Accessor(data.field.clone()),
Self::TupleAccess { index, .. } => Category::TupleAccess(*index),
Self::TupleAccessor(data) => Category::TupleAccessor(data.index),
Self::RecordUpdate { .. } => Category::Record,
Self::Tag {
name, arguments, ..
@ -383,43 +380,30 @@ pub struct ClosureData {
pub loc_body: Box<Loc<Expr>>,
}
/// A tuple accessor like `.2`, which is equivalent to `\x -> x.2`
/// TupleAccessors are desugared to closures; they need to have a name
/// 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 TupleAccessorData {
pub name: Symbol,
pub function_var: Variable,
pub tuple_var: Variable,
pub closure_var: Variable,
pub ext_var: Variable,
pub elem_var: Variable,
pub index: usize,
}
/// A record accessor like `.foo`, which is equivalent to `\r -> r.foo`
/// RecordAccessors 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 RecordAccessorData {
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,
pub field: Lowercase,
/// 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 RecordAccessorData {
impl StructAccessorData {
pub fn to_closure_data(self, record_symbol: Symbol) -> ClosureData {
let RecordAccessorData {
let StructAccessorData {
name,
function_var,
record_var,
@ -436,12 +420,21 @@ impl RecordAccessorData {
// into
//
// (\r -> r.foo)
let body = Expr::RecordAccess {
record_var,
ext_var,
field_var,
loc_expr: Box::new(Loc::at_zero(Expr::Var(record_symbol, record_var))),
field,
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);
@ -1080,15 +1073,18 @@ pub fn canonicalize_expr<'a>(
output,
)
}
ast::Expr::RecordAccessorFunction(field) => (
RecordAccessor(RecordAccessorData {
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: (*field).into(),
field: match field {
Accessor::RecordField(field) => IndexOrField::Field((*field).into()),
Accessor::TupleIndex(index) => IndexOrField::Index(index.parse().unwrap()),
},
}),
Output::default(),
),
@ -1106,18 +1102,6 @@ pub fn canonicalize_expr<'a>(
output,
)
}
ast::Expr::TupleAccessorFunction(index) => (
TupleAccessor(TupleAccessorData {
name: scope.gen_unique_symbol(),
function_var: var_store.fresh(),
tuple_var: var_store.fresh(),
ext_var: var_store.fresh(),
closure_var: var_store.fresh(),
elem_var: var_store.fresh(),
index: index.parse().unwrap(),
}),
Output::default(),
),
ast::Expr::Tag(tag) => {
let variant_var = var_store.fresh();
let ext_var = var_store.fresh();
@ -1874,7 +1858,6 @@ pub fn inline_calls(var_store: &mut VarStore, expr: Expr) -> Expr {
| other @ RuntimeError(_)
| other @ EmptyRecord
| other @ RecordAccessor { .. }
| other @ TupleAccessor { .. }
| other @ RecordUpdate { .. }
| other @ Var(..)
| other @ AbilityMember(..)
@ -3004,7 +2987,6 @@ pub(crate) fn get_lookup_symbols(expr: &Expr) -> Vec<ExpectLookup> {
| Expr::Str(_)
| Expr::ZeroArgumentTag { .. }
| Expr::RecordAccessor(_)
| Expr::TupleAccessor(_)
| Expr::SingleQuote(..)
| Expr::EmptyRecord
| Expr::TypedHole(_)

View file

@ -837,21 +837,10 @@ fn fix_values_captured_in_closure_defs(
no_capture_symbols: &mut VecSet<Symbol>,
closure_captures: &mut VecMap<Symbol, Vec<(Symbol, Variable)>>,
) {
// 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),
);
}
for def in defs.iter_mut() {
fix_values_captured_in_closure_def(def, no_capture_symbols, closure_captures);
}
// 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 = Vec::default();
let mut total_capture_set = VecMap::default();
for def in defs.iter_mut() {
if let Expr::Closure(ClosureData {
captured_symbols, ..
@ -860,8 +849,16 @@ fn fix_values_captured_in_closure_defs(
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);
total_capture_set.dedup_by_key(|(sym, _)| *sym);
for def in defs.iter_mut() {
if let Expr::Closure(ClosureData {
captured_symbols, ..
@ -870,6 +867,10 @@ fn fix_values_captured_in_closure_defs(
*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(
@ -918,6 +919,15 @@ fn fix_values_captured_in_closure_pattern(
}
}
}
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(
@ -1023,9 +1033,9 @@ fn fix_values_captured_in_closure_expr(
captured_symbols.retain(|(s, _)| s != name);
let original_captures_len = captured_symbols.len();
let mut num_visited = 0;
let mut i = 0;
while num_visited < original_captures_len {
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.
@ -1039,19 +1049,21 @@ fn fix_values_captured_in_closure_expr(
let (captured_symbol, _) = captured_symbols[i];
if let Some(captures) = closure_captures.get(&captured_symbol) {
debug_assert!(!captures.is_empty());
captured_symbols.swap_remove(i);
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;
}
num_visited += 1;
}
if captured_symbols.len() > original_captures_len {
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() {
@ -1087,8 +1099,7 @@ fn fix_values_captured_in_closure_expr(
| TypedHole { .. }
| RuntimeError(_)
| ZeroArgumentTag { .. }
| RecordAccessor { .. }
| TupleAccessor { .. } => {}
| RecordAccessor { .. } => {}
List { loc_elems, .. } => {
for elem in loc_elems.iter_mut() {

View file

@ -130,8 +130,7 @@ pub fn desugar_expr<'a>(arena: &'a Bump, loc_expr: &'a Loc<Expr<'a>>) -> &'a Loc
| NonBase10Int { .. }
| Str(_)
| SingleQuote(_)
| RecordAccessorFunction(_)
| TupleAccessorFunction(_)
| AccessorFunction(_)
| Var { .. }
| Underscore { .. }
| MalformedIdent(_, _)

View file

@ -58,6 +58,11 @@ pub enum Pattern {
ext_var: Variable,
destructs: Vec<Loc<RecordDestruct>>,
},
TupleDestructure {
whole_var: Variable,
ext_var: Variable,
destructs: Vec<Loc<TupleDestruct>>,
},
List {
list_var: Variable,
elem_var: Variable,
@ -100,6 +105,7 @@ impl Pattern {
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,
..
@ -130,7 +136,21 @@ impl Pattern {
| UnsupportedPattern(..)
| MalformedPattern(..)
| AbilityMemberSpecialization { .. } => true,
RecordDestructure { destructs, .. } => destructs.is_empty(),
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(_, _) => false,
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 { .. }
@ -160,6 +180,7 @@ impl Pattern {
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,
@ -215,6 +236,13 @@ pub struct RecordDestruct {
pub typ: DestructType,
}
#[derive(Clone, Debug)]
pub struct TupleDestruct {
pub var: Variable,
pub destruct_index: usize,
pub typ: (Variable, Loc<Pattern>),
}
#[derive(Clone, Debug)]
pub enum DestructType {
Required,
@ -554,8 +582,38 @@ pub fn canonicalize_pattern<'a>(
)
}
Tuple(_patterns) => {
todo!("canonicalize_pattern: Tuple")
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) => {
@ -861,7 +919,8 @@ pub enum BindingsFromPattern<'a> {
pub enum BindingsFromPatternWork<'a> {
Pattern(&'a Loc<Pattern>),
Destruct(&'a Loc<RecordDestruct>),
RecordDestruct(&'a Loc<RecordDestruct>),
TupleDestruct(&'a Loc<TupleDestruct>),
}
impl<'a> BindingsFromPattern<'a> {
@ -911,8 +970,12 @@ impl<'a> BindingsFromPattern<'a> {
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(Destruct);
let it = destructs.iter().rev().map(RecordDestruct);
stack.extend(it);
}
NumLiteral(..)
@ -930,7 +993,7 @@ impl<'a> BindingsFromPattern<'a> {
}
}
}
BindingsFromPatternWork::Destruct(loc_destruct) => {
BindingsFromPatternWork::RecordDestruct(loc_destruct) => {
match &loc_destruct.value.typ {
DestructType::Required | DestructType::Optional(_, _) => {
return Some((loc_destruct.value.symbol, loc_destruct.region));
@ -941,6 +1004,10 @@ impl<'a> BindingsFromPattern<'a> {
}
}
}
BindingsFromPatternWork::TupleDestruct(loc_destruct) => {
let inner = &loc_destruct.value.typ.1;
stack.push(BindingsFromPatternWork::Pattern(inner))
}
}
}

View file

@ -9,9 +9,9 @@ use crate::{
def::{Annotation, Declaration, Def},
expr::{
self, AnnotatedMark, ClosureData, Declarations, Expr, Field, OpaqueWrapFunctionData,
RecordAccessorData, TupleAccessorData,
StructAccessorData,
},
pattern::{DestructType, Pattern, RecordDestruct},
pattern::{DestructType, Pattern, RecordDestruct, TupleDestruct},
};
macro_rules! visit_list {
@ -242,7 +242,7 @@ pub fn walk_expr<V: Visitor>(visitor: &mut V, expr: &Expr, var: Variable) {
record_var: _,
ext_var: _,
} => visitor.visit_expr(&loc_expr.value, loc_expr.region, *field_var),
Expr::RecordAccessor(RecordAccessorData { .. }) => { /* terminal */ }
Expr::RecordAccessor(StructAccessorData { .. }) => { /* terminal */ }
Expr::TupleAccess {
elem_var,
loc_expr,
@ -250,7 +250,6 @@ pub fn walk_expr<V: Visitor>(visitor: &mut V, expr: &Expr, var: Variable) {
tuple_var: _,
ext_var: _,
} => visitor.visit_expr(&loc_expr.value, loc_expr.region, *elem_var),
Expr::TupleAccessor(TupleAccessorData { .. }) => { /* terminal */ }
Expr::OpaqueWrapFunction(OpaqueWrapFunctionData { .. }) => { /* terminal */ }
Expr::RecordUpdate {
record_var: _,
@ -483,6 +482,16 @@ pub trait Visitor: Sized {
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<V: Visitor>(visitor: &mut V, pattern: &Pattern) {
@ -503,6 +512,9 @@ pub fn walk_pattern<V: Visitor>(visitor: &mut V, pattern: &Pattern) {
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

View file

@ -1,16 +1,17 @@
[package]
name = "roc_collections"
version = "0.0.1"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
edition = "2021"
description = "Domain-specific collections created for the needs of the compiler."
authors.workspace = true
edition.workspace = true
license.workspace = true
version.workspace = true
[dependencies]
fnv.workspace = true
im.workspace = true
im-rc.workspace = true
wyhash.workspace = true
bumpalo.workspace = true
hashbrown.workspace = true
bitvec.workspace = true
bumpalo.workspace = true
fnv.workspace = true
hashbrown.workspace = true
im-rc.workspace = true
im.workspace = true
wyhash.workspace = true

View file

@ -1,18 +1,20 @@
[package]
name = "roc_constrain"
version = "0.0.1"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
edition = "2021"
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_region = { path = "../region" }
roc_module = { path = "../module" }
roc_parse = { path = "../parse" }
roc_problem = { path = "../problem" }
roc_region = { path = "../region" }
roc_types = { path = "../types" }
roc_can = { path = "../can" }
arrayvec = "0.7.2"
arrayvec.workspace = true

View file

@ -17,7 +17,7 @@ use roc_can::expected::PExpected;
use roc_can::expr::Expr::{self, *};
use roc_can::expr::{
AnnotatedMark, ClosureData, DeclarationTag, Declarations, DestructureDef, ExpectLookup, Field,
FunctionDef, OpaqueWrapFunctionData, RecordAccessorData, TupleAccessorData, WhenBranch,
FunctionDef, OpaqueWrapFunctionData, StructAccessorData, WhenBranch,
};
use roc_can::pattern::Pattern;
use roc_can::traverse::symbols_introduced_from_pattern;
@ -30,7 +30,7 @@ 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,
AliasKind, AnnotationSource, Category, IndexOrField, OptAbleType, PReason, Reason, RecordField,
TypeExtension, TypeTag, Types,
};
@ -1162,7 +1162,7 @@ pub fn constrain_expr(
[constraint, eq, record_con],
)
}
RecordAccessor(RecordAccessorData {
RecordAccessor(StructAccessorData {
name: closure_name,
function_var,
field,
@ -1176,19 +1176,32 @@ pub fn constrain_expr(
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_non_annotation_type(ext_type),
);
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::RecordAccessor(field.clone());
let category = Category::Accessor(field.clone());
let record_expected = constraints.push_expected_type(NoExpectation(record_type_index));
let record_con =
@ -1288,88 +1301,6 @@ pub fn constrain_expr(
let eq = constraints.equal_types_var(elem_var, expected, category, region);
constraints.exists_many([*tuple_var, elem_var, ext_var], [constraint, eq, tuple_con])
}
TupleAccessor(TupleAccessorData {
name: closure_name,
function_var,
tuple_var,
closure_var,
ext_var,
elem_var,
index,
}) => {
let ext_var = *ext_var;
let ext_type = Variable(ext_var);
let elem_var = *elem_var;
let elem_type = Variable(elem_var);
let mut elem_types = VecMap::with_capacity(1);
elem_types.insert(*index, elem_type.clone());
let record_type = Type::Tuple(
elem_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::TupleAccessor(*index);
let record_expected = constraints.push_expected_type(NoExpectation(record_type_index));
let record_con =
constraints.equal_types_var(*tuple_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(elem_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(
[*tuple_var, *function_var, *closure_var, elem_var, ext_var],
cons,
)
}
LetRec(defs, loc_ret, cycle_mark) => {
let body_con = constrain_expr(
types,
@ -4001,10 +3932,6 @@ fn is_generalizable_expr(mut expr: &Expr) -> bool {
// RecordAccessor functions `.field` are equivalent to closures `\r -> r.field`, no need to weaken them.
return true;
}
TupleAccessor(_) => {
// TupleAccessor functions `.0` are equivalent to closures `\r -> r.0`, 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;

View file

@ -3,7 +3,7 @@ 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};
use roc_can::pattern::{DestructType, ListPatterns, RecordDestruct, TupleDestruct};
use roc_collections::all::{HumanIndex, SendMap};
use roc_collections::VecMap;
use roc_module::ident::Lowercase;
@ -125,6 +125,10 @@ fn headers_from_annotation_help(
_ => false,
},
TupleDestructure { destructs: _, .. } => {
todo!();
}
List { patterns, .. } => {
if let Some((_, Some(rest))) = patterns.opt_rest {
let annotation_index = {
@ -465,6 +469,96 @@ pub fn constrain_pattern(
));
}
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<usize, Type> = 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_guard) = 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_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,
);
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,

View file

@ -1,9 +1,10 @@
[package]
name = "roc_debug_flags"
version = "0.0.1"
edition = "2021"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
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]

View file

@ -1,25 +1,27 @@
[package]
name = "roc_derive"
version = "0.0.1"
authors = ["The Roc Contributors"]
license = "UPL-1.0"
edition = "2021"
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_collections = { path = "../collections" }
roc_error_macros = { path = "../../error_macros" }
roc_derive_key = { path = "../derive_key" }
roc_region = { path = "../region" }
roc_module = { path = "../module" }
roc_types = { path = "../types" }
roc_can = { path = "../can" }
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_types = { path = "../types" }
roc_unify = { path = "../unify" }
bumpalo.workspace = true
[features]
default = []
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.

File diff suppressed because it is too large Load diff

View file

@ -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 | elem has Decoding, fmt has 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 | elem has Decoding, fmt has EncoderFormatting
let (elem_decoder, elem_decoder_var) = {
// build `Decode.decoder : Decoder elem fmt` type
// Decoder val fmt | val has Decoding, fmt has 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 | fmt has 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 | fmt has 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),
)
}

View file

@ -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 | a has Decoding, b has Decoding, fmt has 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<Lowercase>,
) -> (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<Lowercase>,
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<Variable>,
result_field_vars: &mut Vec<Variable>,
) -> (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,
},
)
}

View file

@ -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 | a has Decoding, b has Decoding, fmt has 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<Variable>,
state_fields: &mut Vec<Lowercase>,
state_field_vars: &mut Vec<Variable>,
) -> (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,
},
)
}

View file

@ -14,7 +14,8 @@ 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, UnionLambdas, UnionTags, Variable, VariableSubsSlice,
RedundantMark, SubsSlice, TagExt, TupleElems, UnionLambdas, UnionTags, Variable,
VariableSubsSlice,
};
use roc_types::types::RecordField;
@ -50,6 +51,21 @@ pub(crate) fn derive_to_encoder(
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)
.into_iter()
.map(|idx| (idx as usize, env.subs.fresh_unnamed_flex_var()))
.collect::<Vec<_>>();
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
@ -490,6 +506,189 @@ fn to_encoder_record(
(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 | fmt has 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 | fmt has EncoderFormatting
// ~ (typeof tup.0) -[clos]-> t1
env.unify(to_encoder_fn_var, this_to_encoder_fn_var);
// toEncoder : (typeof tup.0) -[clos]-> Encoder fmt | fmt has 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::<Vec<_>>();
// 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 | fmt has 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 | fmt has 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 | fmt has 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,

View file

@ -19,8 +19,8 @@ use roc_types::{
num::int_lit_width_to_variable,
subs::{
Content, ExhaustiveMark, FlatType, GetSubsSlice, LambdaSet, OptVariable, RecordFields,
RedundantMark, Subs, SubsIndex, SubsSlice, TagExt, UnionLambdas, UnionTags, Variable,
VariableSubsSlice,
RedundantMark, Subs, SubsIndex, SubsSlice, TagExt, TupleElems, UnionLambdas, UnionTags,
Variable, VariableSubsSlice,
},
types::RecordField,
};
@ -30,6 +30,7 @@ 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())
@ -122,6 +123,76 @@ fn hash_record(env: &mut Env<'_>, fn_name: Symbol, fields: Vec<Lowercase>) -> (V
)
}
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)
.into_iter()
.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 | hasher has 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<'_>,

Some files were not shown because too many files have changed in this diff Show more