diff --git a/.github/actions/flaky-retry/README.md b/.github/actions/flaky-retry/README.md new file mode 100644 index 0000000000..19bc5b64b9 --- /dev/null +++ b/.github/actions/flaky-retry/README.md @@ -0,0 +1,30 @@ +# Flaky Retry Action + +A GitHub Action that retries a command if it fails with a specific error string. + +## Inputs + +- `command` (required): The command to run +- `error_string_contains` (required): Retry only if error output contains this string +- `retry_count` (optional): Number of times to retry on failure (default: 3) + +## Usage + +```yaml +- name: Run flaky test + uses: ./.github/actions/flaky-retry + with: + command: 'cargo test my_flaky_test' + error_string_contains: 'connection timeout' + retry_count: 5 +``` + +## Behavior + +- Runs the specified command +- If the command succeeds (exit code 0), the action succeeds immediately +- If the command fails (non-zero exit code): + - Checks if the error output contains the specified string + - If it does, retries up to `retry_count` times + - If it doesn't, fails immediately without retrying +- After all retries are exhausted, the action fails with the last exit code \ No newline at end of file diff --git a/.github/actions/flaky-retry/action.yml b/.github/actions/flaky-retry/action.yml new file mode 100644 index 0000000000..8a26761ba7 --- /dev/null +++ b/.github/actions/flaky-retry/action.yml @@ -0,0 +1,101 @@ +name: 'Flaky Retry' +description: 'Retry a command if it fails with a specific error string' +inputs: + retry_count: + description: 'Number of times to retry on failure' + required: false + default: '3' + error_string_contains: + description: 'Retry only if error output contains this string' + required: true + command: + description: 'The command to run' + required: true +runs: + using: 'composite' + steps: + - name: Run command with retry logic (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + attempt=1 + max_attempts=$((${{ inputs.retry_count }} + 1)) + + while [ $attempt -le $max_attempts ]; do + echo "Attempt $attempt of $max_attempts" + echo "Running command: ${{ inputs.command }}" + + # Run command and capture output and exit code + output_file=$(mktemp) + trap 'rm -f "$output_file"' EXIT + + set +e + eval "${{ inputs.command }}" 2>&1 | tee "$output_file" + exit_code=${PIPESTATUS[0]} + set -e + + + # Check if command succeeded + if [ $exit_code -eq 0 ]; then + echo "Command succeeded" + exit 0 + fi + + # Command failed - check if we should retry + if [ $attempt -ge $max_attempts ]; then + echo "Command failed after $max_attempts attempts" + exit $exit_code + fi + + # Check if error matches retry condition + if grep -Eq "${{ inputs.error_string_contains }}" "$output_file"; then + echo "Command failed with exit code $exit_code and error output contains '${{ inputs.error_string_contains }}'" + echo "Retrying..." + attempt=$((attempt + 1)) + sleep 2 + else + echo "Command failed with exit code $exit_code." + exit $exit_code + fi + done + + - name: Run command with retry logic (Windows) + if: runner.os == 'Windows' + shell: powershell + run: | + $attempt = 1 + $maxAttempts = [int]"${{ inputs.retry_count }}" + 1 + + while ($attempt -le $maxAttempts) { + Write-Host "Attempt $attempt of $maxAttempts" + Write-Host "Running command: ${{ inputs.command }}" + + # Run command and capture output + $ErrorActionPreference = 'Continue' + $output = & cmd /c "${{ inputs.command }} 2>&1" + $exitCode = $LASTEXITCODE + Write-Host $output + + # Check if command succeeded + if ($exitCode -eq 0) { + Write-Host "Command succeeded" + exit 0 + } + + # Command failed - check if we should retry + if ($attempt -ge $maxAttempts) { + Write-Host "Command failed after $maxAttempts attempts" + exit $exitCode + } + + # Check if error matches retry condition + if ($output -match "${{ inputs.error_string_contains }}") { + Write-Host "Command failed with exit code $exitCode and error output contains '${{ inputs.error_string_contains }}'" + Write-Host "Retrying..." + $attempt++ + Start-Sleep -Seconds 2 + } else { + Write-Host "Command failed with exit code $exitCode." + exit $exitCode + } + } diff --git a/.github/workflows/alwayscheck.yml b/.github/workflows/alwayscheck.yml index 7d5fdd107e..42ef259010 100644 --- a/.github/workflows/alwayscheck.yml +++ b/.github/workflows/alwayscheck.yml @@ -32,9 +32,9 @@ jobs: - name: setup Roc run: | - curl -s -OL https://github.com/roc-lang/roc/releases/download/alpha3-rolling/roc-linux_x86_64-alpha3-rolling.tar.gz - tar -xf roc-linux_x86_64-alpha3-rolling.tar.gz - rm roc-linux_x86_64-alpha3-rolling.tar.gz + curl -s -OL https://github.com/roc-lang/roc/releases/download/alpha4-rolling/roc-linux_x86_64-alpha4-rolling.tar.gz + tar -xf roc-linux_x86_64-alpha4-rolling.tar.gz + rm roc-linux_x86_64-alpha4-rolling.tar.gz cd roc_nightly-* # make roc binary available echo "$(pwd)" >> $GITHUB_PATH diff --git a/.github/workflows/basic_cli_build_release.yml b/.github/workflows/basic_cli_build_release.yml index f69baf9891..ac749cc490 100644 --- a/.github/workflows/basic_cli_build_release.yml +++ b/.github/workflows/basic_cli_build_release.yml @@ -96,7 +96,7 @@ jobs: basic-cli/platform/linux-arm64.a build-macos-x86_64-files: - runs-on: [macos-13] # I expect the generated files to work on macOS 13 and up + runs-on: [macos-15-intel] # should work on macOS 15+ needs: [prepare] steps: - uses: actions/checkout@v4 @@ -207,7 +207,7 @@ jobs: needs: [create-release-archive] strategy: matrix: - os: [ubuntu-22.04, ubuntu-24.04, ubuntu-24.04-arm, macos-13, macos-14, macos-15] + os: [ubuntu-22.04, ubuntu-24.04, ubuntu-24.04-arm, macos-15-intel, macos-14, macos-15] runs-on: ${{ matrix.os }} steps: - name: Download the previously uploaded files @@ -220,7 +220,7 @@ jobs: echo "os_pattern=linux_arm64" >> $GITHUB_OUTPUT elif [[ "${{ matrix.os }}" =~ ^ubuntu- ]]; then echo "os_pattern=linux_x86_64" >> $GITHUB_OUTPUT - elif [ "${{ matrix.os }}" = "macos-13" ]; then + elif [ "${{ matrix.os }}" = "macos-15-intel" ]; then echo "os_pattern=macos_x86_64" >> $GITHUB_OUTPUT else echo "os_pattern=macos_apple_silicon" >> $GITHUB_OUTPUT diff --git a/.github/workflows/basic_cli_test_arm64.yml b/.github/workflows/basic_cli_test_arm64.yml index 71b0d2a1d2..9ce528ee0a 100644 --- a/.github/workflows/basic_cli_test_arm64.yml +++ b/.github/workflows/basic_cli_test_arm64.yml @@ -56,5 +56,9 @@ jobs: git fetch --tags latestTag=$(git describe --tags $(git rev-list --tags --max-count=1)) git checkout $latestTag + # remove things that don't work on musl + rm ./examples/file-accessed-modified-created-time.roc + sed -i.bak -e '/time_accessed!,$/d' -e '/time_modified!,$/d' -e '/time_created!,$/d' -e '/^time_accessed!/,/^$/d' -e '/^time_modified!/,/^$/d' -e '/^time_created!/,/^$/d' -e '/^import Utc exposing \[Utc\]$/d' ./platform/File.roc + rm ./platform/File.roc.bak sed -i 's/x86_64/arm64/g' ./ci/test_latest_release.sh - EXAMPLES_DIR=./examples/ ./ci/test_latest_release.sh + GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} EXAMPLES_DIR=./examples/ ./ci/test_latest_release.sh diff --git a/.github/workflows/basic_webserver_build_release.yml b/.github/workflows/basic_webserver_build_release.yml index 1aac671447..b57cf163e2 100644 --- a/.github/workflows/basic_webserver_build_release.yml +++ b/.github/workflows/basic_webserver_build_release.yml @@ -77,7 +77,7 @@ jobs: basic-webserver/platform/linux-arm64.a build-macos-x86_64-files: - runs-on: [macos-13] # I expect the generated files to work on macOS 13 and up + runs-on: [macos-15-intel] # should work on macOS 15+ needs: [fetch-releases] steps: - uses: actions/checkout@v4 @@ -187,7 +187,7 @@ jobs: needs: [create-release-archive] strategy: matrix: - os: [ubuntu-22.04, ubuntu-24.04, ubuntu-24.04-arm, macos-13, macos-14, macos-15] + os: [ubuntu-22.04, ubuntu-24.04, ubuntu-24.04-arm, macos-15-intel, macos-14, macos-15] runs-on: ${{ matrix.os }} steps: - name: Download the previously uploaded files @@ -200,7 +200,7 @@ jobs: echo "os_pattern=linux_arm64" >> $GITHUB_OUTPUT elif [[ "${{ matrix.os }}" =~ ^ubuntu- ]]; then echo "os_pattern=linux_x86_64" >> $GITHUB_OUTPUT - elif [ "${{ matrix.os }}" = "macos-13" ]; then + elif [ "${{ matrix.os }}" = "macos-15-intel" ]; then echo "os_pattern=macos_x86_64" >> $GITHUB_OUTPUT else echo "os_pattern=macos_apple_silicon" >> $GITHUB_OUTPUT diff --git a/.github/workflows/ci_cross_compile.yml b/.github/workflows/ci_cross_compile.yml new file mode 100644 index 0000000000..fd7849f8d1 --- /dev/null +++ b/.github/workflows/ci_cross_compile.yml @@ -0,0 +1,139 @@ +on: + workflow_call: + +name: Cross Compilation Test + +# Do not add permissions here! Configure them at the job level! +permissions: {} + +jobs: + # Cross-compile all fx platform tests from all host platforms + cross-compile-fx-tests: + runs-on: ${{ matrix.host }} + strategy: + fail-fast: false + matrix: + host: [ + ubuntu-22.04, # Linux x64 host + macos-15-intel, # macOS x64 host + macos-15, # macOS ARM64 host + windows-2022, # Windows x64 host + ] + target: [ + x64musl, # Linux x86_64 musl (static linking) + arm64musl, # Linux ARM64 musl (static linking) + ] + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + + - uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # 2.0.5 + with: + version: 0.15.2 + use-cache: false + + - name: Setup MSVC (Windows) + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # ratchet:ilammy/msvc-dev-cmd@v1.13.0 + with: + arch: x64 + + - name: Build roc compiler and test_runner + uses: ./.github/actions/flaky-retry + with: + command: 'zig build' + error_string_contains: 'EndOfStream|503|Timeout' + retry_count: 3 + + - name: Run fx platform cross-compilation tests (Unix) + if: runner.os != 'Windows' + run: | + echo "Cross-compiling fx tests from ${{ matrix.host }} to ${{ matrix.target }}" + ./zig-out/bin/test_runner ./zig-out/bin/roc fx --target=${{ matrix.target }} --mode=cross + + - name: Run fx platform cross-compilation tests (Windows) + if: runner.os == 'Windows' + run: | + echo "Cross-compiling fx tests from ${{ matrix.host }} to ${{ matrix.target }}" + zig-out\bin\test_runner.exe zig-out\bin\roc.exe fx --target=${{ matrix.target }} --mode=cross + + - name: Cross-compile int platform test app (Unix) + if: runner.os != 'Windows' + run: | + echo "Cross-compiling int platform from ${{ matrix.host }} to ${{ matrix.target }}" + ./zig-out/bin/roc build --target=${{ matrix.target }} --output=int_app_${{ matrix.target }}_${{ matrix.host }} test/int/app.roc + + - name: Cross-compile int platform test app (Windows) + if: runner.os == 'Windows' + run: | + echo "Cross-compiling int platform from ${{ matrix.host }} to ${{ matrix.target }}" + zig-out\bin\roc.exe build --target=${{ matrix.target }} --output=int_app_${{ matrix.target }}_${{ matrix.host }} test/int/app.roc + + - name: Upload cross-compiled int app executables + uses: actions/upload-artifact@v4 # ratchet:actions/upload-artifact@v4 + with: + name: cross-compiled-${{ matrix.host }}-${{ matrix.target }} + path: | + int_app_${{ matrix.target }}_* + retention-days: 1 + + # Test cross-compiled int platform executables on actual target platforms + test-int-on-target: + needs: [cross-compile-fx-tests] + runs-on: ${{ matrix.target_os }} + strategy: + fail-fast: false + matrix: + include: + # Test x64musl executables on Linux x64 + - target: x64musl + target_os: ubuntu-22.04 + arch: x64 + # Test arm64musl executables on Linux ARM64 + - target: arm64musl + target_os: ubuntu-24.04-arm + arch: arm64 + steps: + - name: Download all cross-compiled artifacts + uses: actions/download-artifact@v4 + with: + pattern: cross-compiled-*-${{ matrix.target }} + merge-multiple: true + + - name: List downloaded files + run: | + echo "Downloaded cross-compiled executables:" + ls -la *_${{ matrix.target }}* || echo "No files found" + + - name: Test cross-compiled executables from all hosts + run: | + success_count=0 + total_count=0 + + echo "Testing ${{ matrix.target }} executables on ${{ matrix.target_os }} (${{ matrix.arch }})" + + # Test int apps from all host platforms + for int_app in int_app_${{ matrix.target }}_*; do + if [ -f "$int_app" ]; then + echo "" + echo "Testing $int_app:" + chmod +x "$int_app" + if ./"$int_app"; then + echo "✅ $int_app: SUCCESS" + success_count=$((success_count + 1)) + else + echo "❌ $int_app: FAILED" + fi + total_count=$((total_count + 1)) + fi + done + + echo "" + echo "Summary: $success_count/$total_count executables passed" + + if [ $success_count -eq $total_count ] && [ $total_count -gt 0 ]; then + echo "🎉 All cross-compiled executables work correctly!" + else + echo "💥 Some cross-compiled executables failed" + exit 1 + fi diff --git a/.github/workflows/ci_manager.yml b/.github/workflows/ci_manager.yml index 1f17b4115a..6dfec19de4 100644 --- a/.github/workflows/ci_manager.yml +++ b/.github/workflows/ci_manager.yml @@ -31,7 +31,12 @@ jobs: - 'build.zig' - 'build.zig.zon' - '.github/workflows/ci_zig.yml' + - '.github/workflows/ci_cross_compile.yml' + - '.github/actions/flaky-retry/action.yml' - 'ci/zig_lints.sh' + - 'ci/check_test_wiring.zig' + - 'ci/custom_valgrind.sh' + - 'ci/valgrind.supp' - uses: dorny/paths-filter@v3 id: other_filter with: @@ -46,10 +51,16 @@ jobs: - '!build.zig' - '!build.zig.zon' - '!.github/workflows/ci_zig.yml' + - '!.github/workflows/ci_cross_compile.yml' + - '!.github/actions/flaky-retry/action.yml' - '!ci/zig_lints.sh' + - '!ci/check_test_wiring.zig' + - '!ci/custom_valgrind.sh' + - '!ci/valgrind.supp' # Files that ci manager workflows should not run on. - '!.gitignore' - '!.reuse' + - '!.rules' - '!authors' - '!legal_details' - '!LICENSE' diff --git a/.github/workflows/ci_zig.yml b/.github/workflows/ci_zig.yml index ce69c9f8c0..35a035a9d4 100644 --- a/.github/workflows/ci_zig.yml +++ b/.github/workflows/ci_zig.yml @@ -1,5 +1,6 @@ on: workflow_call: + workflow_dispatch: name: CI New Compiler @@ -13,22 +14,27 @@ jobs: steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4 - - uses: mlugg/setup-zig@475c97be87a204e6c57fe851f970bd02005a70f0 # ratchet:mlugg/setup-zig@v2 + - uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # ratchet:mlugg/setup-zig@v2.0.5 with: - version: 0.14.1 - # Do not cache the zig cache. - # Just cache the zig executable. - # Zig cache corrupts sometimes and uses tons of space. - use-cache: false + version: 0.15.2 + use-cache: true - name: zig lints run: | - ./ci/zig_lints.sh + zig run ci/zig_lints.zig zig build check-fmt - # We run zig check with tracy enabled. - # This ensure it compiles and doesn't go stale. + - name: Check that test is actually wired up + run: | + zig run ci/check_test_wiring.zig + + - name: Check fx platform test coverage + run: | + zig build checkfx + + # We run zig check with tracy enabled. + # This ensure it compiles and doesn't go stale. - name: Checkout Tracy uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4 with: @@ -38,7 +44,6 @@ jobs: - name: zig check run: | - # -Dllvm incurs a costly download step, leave that for later. # Just the do super fast check step for now. zig build -Dno-bin -Dfuzz -Dtracy=./tracy @@ -65,7 +70,11 @@ jobs: - name: Run Playground Tests run: | - zig build playground-test -Doptimize=ReleaseSmall -- --verbose + zig build test-playground -Doptimize=ReleaseSmall -- --verbose + + - name: Verify Serialized Type Sizes + run: | + zig build test-serialization-sizes zig-tests: needs: check-once @@ -73,51 +82,84 @@ jobs: strategy: fail-fast: false matrix: - os: [ - macos-13, - macos-15, - ubuntu-22.04, - ubuntu-24.04-arm, - windows-2022, - windows-2025, - windows-11-arm, - ] # macos-13 uses x64, macos-15 uses arm64 + include: + - os: macos-15-intel + cpu_flag: -Dcpu=x86_64_v3 + target_flag: '' + - os: macos-15 + cpu_flag: '' + target_flag: '' + - os: ubuntu-22.04 + cpu_flag: -Dcpu=x86_64_v3 + target_flag: -Dtarget=x86_64-linux-musl + - os: ubuntu-24.04-arm + cpu_flag: '' + target_flag: -Dtarget=aarch64-linux-musl + - os: windows-2022 + cpu_flag: -Dcpu=x86_64_v3 + target_flag: '' + - os: windows-2025 + cpu_flag: -Dcpu=x86_64_v3 + target_flag: '' + - os: windows-11-arm + cpu_flag: '' + target_flag: '' + steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4 - - uses: mlugg/setup-zig@475c97be87a204e6c57fe851f970bd02005a70f0 # ratchet:mlugg/setup-zig@v2 + - uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # ratchet:mlugg/setup-zig@v2.0.5 with: - version: 0.14.1 - # Do not cache the zig cache. - # Just cache the zig executable. - # Zig cache corrupts sometimes and uses tons of space. - use-cache: false + version: 0.15.2 + use-cache: true - if: startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos') uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # ratchet:cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-25.05 - - name: build roc + # temp fix, see https://roc.zulipchat.com/#narrow/channel/395097-compiler-development/topic/CI/near/542085291 + - name: delete llvm-config + if: startsWith(matrix.os, 'ubuntu') && endsWith(matrix.os, '-arm') run: | - zig build -Dllvm -Dfuzz -Dsystem-afl=false + sudo rm /usr/lib/llvm-18/bin/llvm-config + + - name: build roc + repro executables + uses: ./.github/actions/flaky-retry + with: + command: "zig build -Dfuzz -Dsystem-afl=false -Doptimize=ReleaseFast ${{ matrix.cpu_flag }} ${{ matrix.target_flag }}" + error_string_contains: "EndOfStream|Timeout" + retry_count: 3 + + - name: Check Builtin.roc formatting (Unix) + if: runner.os != 'Windows' + run: | + ./zig-out/bin/roc fmt --check src/build/roc/Builtin.roc + + - name: Check Builtin.roc formatting (Windows) + if: runner.os == 'Windows' + run: | + zig-out\bin\roc.exe fmt --check src/build/roc/Builtin.roc - name: Run Test Platforms (Unix) if: runner.os != 'Windows' run: | - ./zig-out/bin/roc --no-cache test/str/app.roc - ./zig-out/bin/roc --no-cache test/int/app.roc + zig build test-cli ${{ matrix.target_flag }} - name: Setup MSVC (Windows) if: runner.os == 'Windows' - uses: ilammy/msvc-dev-cmd@v1 + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # ratchet:ilammy/msvc-dev-cmd@v1.13.0 with: arch: ${{ matrix.os == 'windows-11-arm' && 'arm64' || 'x64' }} - name: Run Test Platforms (Windows) if: runner.os == 'Windows' run: | - zig-out\bin\roc.exe --no-cache test/str/app.roc - zig-out\bin\roc.exe --no-cache test/int/app.roc + zig build test-cli + + - name: Build Test Platforms (cross-compile) + if: runner.os != 'Windows' + run: | + ./zig-out/bin/test_runner ./zig-out/bin/roc int --mode=cross - name: roc executable minimal check (Unix) if: runner.os != 'Windows' @@ -130,11 +172,23 @@ jobs: zig-out\bin\roc.exe check ./src/PROFILING/bench_repeated_check.roc - name: zig snapshot tests - run: zig build snapshot -- --debug + run: zig build snapshot ${{ matrix.target_flag }} -- --debug - - name: zig tests - run: | - zig build test -Dllvm -Dfuzz -Dsystem-afl=false + # 1) in debug mode + - name: build and execute tests, build repro executables + uses: ./.github/actions/flaky-retry + with: + command: "zig build test -Dfuzz -Dsystem-afl=false ${{ matrix.target_flag }}" + error_string_contains: "double roundtrip bundle|connectTcp|downloadToFile" + retry_count: 3 + + # 2) in release mode + - name: Build and execute tests, build repro executables. All in release mode. + uses: ./.github/actions/flaky-retry + with: + command: "zig build test -Doptimize=ReleaseFast -Dfuzz -Dsystem-afl=false ${{ matrix.cpu_flag }} ${{ matrix.target_flag }}" + error_string_contains: "double roundtrip bundle|connectTcp|downloadToFile" + retry_count: 3 - name: Check for snapshot changes run: | @@ -143,22 +197,33 @@ jobs: echo "" echo "OOPS! It looks like the snapshots in 'test/snapshots' have changed."; echo "Please run 'zig build snapshot' locally, review the updates, and commit the changes."; + echo "" + echo "NOTE: If snapshots pass locally but fail in CI, you may need to merge origin/main"; + echo "into your branch first, then regenerate snapshots. CI tests your branch merged with"; + echo "main, so new snapshots from main can cause diffs when regenerated with your changes."; + echo "" echo "Here's what changed:"; echo "" git diff test/snapshots; exit 1; } - - name: check snapshots with valgrind (ubuntu) + - name: test with valgrind (ubuntu) # Sadly, with zig 0.14.1, this had to be disabled on arm. # Valgrind as of 3.25.1 does not support some arm instructions zig generates. # So valgrind only works happily on x86_64 linux. # We can re-evaluate as new version of zig/valgrind come out. if: ${{ matrix.os == 'ubuntu-22.04' }} run: | - sudo apt install -y valgrind + # Install libc6-dbg which is required for Valgrind's function redirections + sudo apt-get update && sudo apt-get install -y libc6-dbg + sudo snap install valgrind --classic valgrind --version - valgrind --leak-check=full --error-exitcode=1 --errors-for-leak-kinds=definite,possible ./zig-out/bin/snapshot --debug + ./ci/custom_valgrind.sh ./zig-out/bin/snapshot --debug --verbose + ./ci/custom_valgrind.sh ./zig-out/bin/roc --no-cache test/str/app.roc + ./ci/custom_valgrind.sh ./zig-out/bin/roc --no-cache test/int/app.roc + # valgrind all app files in test/fx except those that read from stdin + # find test/fx -maxdepth 1 -name "*.roc" -type f -exec grep -l "app \[main" {} + | xargs -I {} sh -c 'grep -L "Stdin" {} || true' | xargs -I {} ./ci/custom_valgrind.sh ./zig-out/bin/roc --no-cache {} - name: check if statically linked (ubuntu) if: startsWith(matrix.os, 'ubuntu') @@ -188,11 +253,11 @@ jobs: } - name: Test inside nix too - if: startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos') + if: ${{ runner.os == 'Linux' || (runner.os == 'macOS' && runner.arch != 'X64') }} run: | git clean -fdx git reset --hard HEAD - nix develop ./src/flake.nix -c zig build snapshot && zig build test + nix develop ./src/ -c zig build ${{ matrix.target_flag }} && zig build snapshot ${{ matrix.target_flag }} && zig build test ${{ matrix.target_flag }} zig-cross-compile: needs: check-once @@ -213,14 +278,17 @@ jobs: steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4 - - uses: mlugg/setup-zig@475c97be87a204e6c57fe851f970bd02005a70f0 # ratchet:mlugg/setup-zig@v2 + - uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # ratchet:mlugg/setup-zig@v2.0.5 with: - version: 0.14.1 - # Do not cache the zig cache. - # Just cache the zig executable. - # Zig cache corrupts sometimes and uses tons of space. - use-cache: false + version: 0.15.2 + use-cache: true - name: cross compile with llvm - run: | - ./ci/retry_flaky.sh zig build -Dtarget=${{ matrix.target }} -Dllvm + uses: ./.github/actions/flaky-retry + with: + command: zig build -Dtarget=${{ matrix.target }} + error_string_contains: "error: bad HTTP response code: '500 Internal Server Error'|TemporaryNameServerFailure|503|Timeout" + + # Test cross-compilation with Roc's cross-compilation system (musl + glibc) + roc-cross-compile: + uses: ./.github/workflows/ci_cross_compile.yml diff --git a/.github/workflows/nightly_linux_arm64.yml b/.github/workflows/nightly_linux_arm64.yml index 842d332315..cb5279c496 100644 --- a/.github/workflows/nightly_linux_arm64.yml +++ b/.github/workflows/nightly_linux_arm64.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: mlugg/setup-zig@475c97be87a204e6c57fe851f970bd02005a70f0 + - uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # 2.0.5 with: version: 0.13.0 @@ -66,6 +66,9 @@ jobs: - name: Make nightly release tar archive run: ./ci/package_release.sh ${{ env.RELEASE_FOLDER_NAME }} + - name: Calculate archive hash for security purposes + run: ls | grep "roc_nightly.*tar\.gz" | xargs sha256sum + - name: Upload roc nightly tar. Actually uploading to github releases has to be done manually. uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/nightly_linux_x86_64.yml b/.github/workflows/nightly_linux_x86_64.yml index 548d71c970..247b962ab7 100644 --- a/.github/workflows/nightly_linux_x86_64.yml +++ b/.github/workflows/nightly_linux_x86_64.yml @@ -60,6 +60,9 @@ jobs: - name: Make nightly release tar archive run: ./ci/package_release.sh ${{ env.RELEASE_FOLDER_NAME }} + - name: Calculate archive hash for security purposes + run: ls | grep "roc_nightly.*tar\.gz" | xargs sha256sum + - name: Upload roc nightly tar. Actually uploading to github releases has to be done manually. uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/nightly_macos_apple_silicon.yml b/.github/workflows/nightly_macos_apple_silicon.yml index c74f48f6c8..bc5e9aac05 100644 --- a/.github/workflows/nightly_macos_apple_silicon.yml +++ b/.github/workflows/nightly_macos_apple_silicon.yml @@ -53,6 +53,9 @@ jobs: - name: package release run: ./ci/package_release.sh ${{ env.RELEASE_FOLDER_NAME }} + - name: Calculate archive hash for security purposes + run: ls | grep "roc_nightly.*tar\.gz" | xargs shasum -a 256 + - name: delete everything except the tar run: ls | grep -v "roc_nightly.*tar\.gz" | xargs rm -rf diff --git a/.github/workflows/nightly_macos_x86_64.yml b/.github/workflows/nightly_macos_x86_64.yml index e710356122..622d67924e 100644 --- a/.github/workflows/nightly_macos_x86_64.yml +++ b/.github/workflows/nightly_macos_x86_64.yml @@ -61,6 +61,9 @@ jobs: - name: package release run: ./ci/package_release.sh ${{ env.RELEASE_FOLDER_NAME }} + - name: Calculate archive hash for security purposes + run: ls | grep "roc_nightly.*tar\.gz" | xargs shasum -a 256 + - name: Upload artifact. Actually uploading to github releases has to be done manually. uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/nightly_new_compiler_all_os.yml b/.github/workflows/nightly_new_compiler_all_os.yml new file mode 100644 index 0000000000..fcbc9b9bee --- /dev/null +++ b/.github/workflows/nightly_new_compiler_all_os.yml @@ -0,0 +1,134 @@ +on: +# pull_request: + workflow_dispatch: + schedule: + - cron: "0 9 * * *" + +name: Nightly release for new compiler + +# Do not add permissions here! Configure them at the job level! +permissions: {} + +jobs: + build-and-package: + name: build and package nightly release + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: macos-15-intel + artifact_name: macos_x86_64 + - os: macos-15 + artifact_name: macos_apple_silicon + - os: ubuntu-22.04 + artifact_name: linux_x86_64 + - os: ubuntu-24.04-arm + artifact_name: linux_arm64 + - os: windows-2022 + artifact_name: windows_x86_64 + - os: windows-11-arm + artifact_name: windows_arm64 + + defaults: + run: + shell: bash + + steps: + - name: Checkout + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4 + + - uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # ratchet:mlugg/setup-zig@v2.0.5 + with: + version: 0.15.2 + use-cache: true + + # temp fix, see https://roc.zulipchat.com/#narrow/channel/395097-compiler-development/topic/CI/near/542085291 + - name: delete llvm-config + if: startsWith(matrix.os, 'ubuntu') && endsWith(matrix.os, '-arm') + run: | + sudo rm /usr/lib/llvm-18/bin/llvm-config + + - name: Setup MSVC (Windows) + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # ratchet:ilammy/msvc-dev-cmd@v1.13.0 + with: + arch: ${{ matrix.os == 'windows-11-arm' && 'arm64' || 'x64' }} + + - name: create version.txt + run: ./ci/write_version.sh + + - name: get commit SHA + run: echo "SHA=$(git rev-parse --short "$GITHUB_SHA")" >> $GITHUB_ENV + + - name: get date + run: echo "DATE=$(date "+%Y-%m-%d")" >> $GITHUB_ENV + + - name: build release + uses: ./.github/actions/flaky-retry + with: + command: 'zig build roc -Doptimize=ReleaseFast' + error_string_contains: 'EndOfStream' + retry_count: 3 + + - name: build file name + env: + DATE: ${{ env.DATE }} + SHA: ${{ env.SHA }} + run: echo "RELEASE_FOLDER_NAME=new_roc_nightly-${{ matrix.artifact_name }}-$DATE-$SHA" >> $GITHUB_ENV + + - name: Package release + run: | + mkdir -p ${{ env.RELEASE_FOLDER_NAME }} + cp ./zig-out/bin/roc ${{ env.RELEASE_FOLDER_NAME }}/ + cp LICENSE legal_details ${{ env.RELEASE_FOLDER_NAME }}/ + + - name: Compress release (Unix) + if: runner.os != 'Windows' + run: tar -czvf "${{ env.RELEASE_FOLDER_NAME }}.tar.gz" ${{ env.RELEASE_FOLDER_NAME }} + + - name: Compress release (Windows) + if: runner.os == 'Windows' + run: 7z a -tzip "${{ env.RELEASE_FOLDER_NAME }}.zip" ${{ env.RELEASE_FOLDER_NAME }} + + - name: Calculate archive hash for security purposes (Unix) + if: runner.os != 'Windows' + run: sha256sum ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + + - name: Calculate archive hash for security purposes (Windows) + if: runner.os == 'Windows' + run: sha256sum ${{ env.RELEASE_FOLDER_NAME }}.zip + + - name: Upload roc nightly archive (Unix) + if: runner.os != 'Windows' + uses: actions/upload-artifact@v4 + with: + name: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + path: ${{ env.RELEASE_FOLDER_NAME }}.tar.gz + retention-days: 4 + + - name: Upload roc nightly archive (Windows) + if: runner.os == 'Windows' + uses: actions/upload-artifact@v4 + with: + name: ${{ env.RELEASE_FOLDER_NAME }}.zip + path: ${{ env.RELEASE_FOLDER_NAME }}.zip + retention-days: 4 + + - name: Clean workspace for test + run: | + find . -mindepth 1 -maxdepth 1 ! -name "${{ env.RELEASE_FOLDER_NAME }}.tar.gz" ! -name "${{ env.RELEASE_FOLDER_NAME }}.zip" ! -name "src" -exec rm -rf {} + + find src -mindepth 1 ! -path "src/PROFILING*" -exec rm -rf {} + + find src/PROFILING -mindepth 1 ! -name "bench_repeated_check.roc" -exec rm -rf {} + + + - name: Test archive (Unix) + if: runner.os != 'Windows' + run: | + tar -xzf "${{ env.RELEASE_FOLDER_NAME }}.tar.gz" + ./${{ env.RELEASE_FOLDER_NAME }}/roc check src/PROFILING/bench_repeated_check.roc + + - name: Test archive (Windows) + if: runner.os == 'Windows' + run: | + 7z x "${{ env.RELEASE_FOLDER_NAME }}.zip" + ./${{ env.RELEASE_FOLDER_NAME }}/roc check src/PROFILING/bench_repeated_check.roc diff --git a/.github/workflows/nix_macos_apple_silicon.yml b/.github/workflows/nix_macos_apple_silicon.yml index abb5fbf633..55e43da8ad 100644 --- a/.github/workflows/nix_macos_apple_silicon.yml +++ b/.github/workflows/nix_macos_apple_silicon.yml @@ -13,7 +13,7 @@ jobs: nix-apple-silicon: name: nix-apple-silicon runs-on: [self-hosted, macOS, ARM64] - timeout-minutes: 90 + timeout-minutes: 130 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/nix_macos_x86_64.yml b/.github/workflows/nix_macos_x86_64.yml index 60b12c39f5..ce97dd2a88 100644 --- a/.github/workflows/nix_macos_x86_64.yml +++ b/.github/workflows/nix_macos_x86_64.yml @@ -12,7 +12,7 @@ env: jobs: nix-macos-x86-64: name: nix-macos-x86-64 - runs-on: [macos-13] + runs-on: [macos-15-intel] timeout-minutes: 90 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test_alpha_many_os.yml b/.github/workflows/test_alpha_many_os.yml index 5f110785c5..5e5c00bacc 100644 --- a/.github/workflows/test_alpha_many_os.yml +++ b/.github/workflows/test_alpha_many_os.yml @@ -9,16 +9,16 @@ permissions: {} jobs: test-alpha: - name: test alpha macos 13 (x64), macos 14 (aarch64), ubuntu 22.04-24.04 (x64), ubuntu 22.04-24.04 (aarch64) + name: test alpha macos 15 intel (x64), macos 14 (aarch64), ubuntu 22.04-24.04 (x64), ubuntu 22.04-24.04 (aarch64) strategy: fail-fast: false matrix: - os: [macos-13, macos-14, ubuntu-22.04, ubuntu-24.04, ubuntu-22.04-arm, ubuntu-24.04-arm] + os: [macos-15-intel, macos-14, ubuntu-22.04, ubuntu-24.04, ubuntu-22.04-arm, ubuntu-24.04-arm] runs-on: ${{ matrix.os }} timeout-minutes: 90 steps: - uses: actions/checkout@v4 - - uses: mlugg/setup-zig@v1 + - uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # 2.0.5 with: version: 0.13.0 @@ -46,8 +46,8 @@ jobs: DOWNLOAD_URL=$(curl -sH "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$ASSETS_URL" | jq -r '.[] | select(.name | startswith("roc-linux_arm64-") and (contains("old") | not)) | .browser_download_url') curl -fLH "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$DOWNLOAD_URL" -o roc_release.tar.gz - - name: get the latest release archive for macos 13 (x86_64) - if: matrix.os == 'macos-13' + - name: get the latest release archive for macos 15 intel (x86_64) + if: matrix.os == 'macos-15' run: | ASSETS_URL="${{ steps.get_release.outputs.assets_url }}" DOWNLOAD_URL=$(curl -sH "Authorization: token ${{ secrets.GITHUB_TOKEN }}" "$ASSETS_URL" | jq -r '.[] | select(.name | startswith("roc-macos_x86_64-")) | .browser_download_url') diff --git a/.github/workflows/test_nightly_many_os.yml b/.github/workflows/test_nightly_many_os.yml index b051bfc825..86627f1a7c 100644 --- a/.github/workflows/test_nightly_many_os.yml +++ b/.github/workflows/test_nightly_many_os.yml @@ -9,16 +9,16 @@ permissions: {} jobs: test-nightly: - name: test nightly macos 13 (x64), macos 14 (aarch64), ubuntu 22.04-24.04 (x64), ubuntu 22.04-24.04 (aarch64) + name: test nightly macos 15 intel (x64), macos 14 (aarch64), ubuntu 22.04-24.04 (x64), ubuntu 22.04-24.04 (aarch64) strategy: fail-fast: false matrix: - os: [macos-13, macos-14, ubuntu-22.04, ubuntu-24.04, ubuntu-22.04-arm, ubuntu-24.04-arm] + os: [macos-15-intel, macos-14, ubuntu-22.04, ubuntu-24.04, ubuntu-22.04-arm, ubuntu-24.04-arm] runs-on: ${{ matrix.os }} timeout-minutes: 90 steps: - uses: actions/checkout@v4 - - uses: mlugg/setup-zig@v1 + - uses: mlugg/setup-zig@8d6198c65fb0feaa111df26e6b467fea8345e46f # 2.0.5 with: version: 0.13.0 @@ -35,8 +35,8 @@ jobs: run: | curl -fL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-linux_arm64-latest.tar.gz -o roc_release.tar.gz - - name: get the latest release archive for macos 13 (x86_64) - if: matrix.os == 'macos-13' + - name: get the latest release archive for macos 15 intel (x86_64) + if: matrix.os == 'macos-15-intel' run: curl -fL https://github.com/roc-lang/roc/releases/download/nightly/roc_nightly-macos_x86_64-latest.tar.gz -o roc_release.tar.gz - name: get the latest release archive for macos 14 (aarch64) diff --git a/.github/workflows/ubuntu_x86_64.yml b/.github/workflows/ubuntu_x86_64.yml index 3b9d5d618d..5b8d86519c 100644 --- a/.github/workflows/ubuntu_x86_64.yml +++ b/.github/workflows/ubuntu_x86_64.yml @@ -29,7 +29,7 @@ jobs: - name: zig fmt check, zig tests run: cd crates/compiler/builtins/bitcode && ./run-tests.sh - - name: roc format check on builtins + - name: roc fmt check on builtins run: cargo run --locked --release format --check crates/compiler/builtins/roc - name: ensure there are no unused dependencies diff --git a/.gitignore b/.gitignore index f3dc9d0183..d9af551788 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ # Ignore the following directories and file extensions target +!src/target generated-docs zig-out @@ -32,6 +33,7 @@ zig-out *.rs.bk *.o *.a +*.s *.so *.so.* *.obj @@ -65,6 +67,7 @@ metadata .exrc .vimrc .nvimrc +.zed/ # rust cache (sccache folder) sccache_dir @@ -140,3 +143,5 @@ src/snapshots/**/*.html # Local claude-code settings .claude/settings.local.json + +*.bak diff --git a/.rules b/.rules index bb04bedf41..f770f51f4b 100644 --- a/.rules +++ b/.rules @@ -1,8 +1,8 @@ # Roc Compiler -This repository contains the source code for the Roc compiler -- both the original prototype written in Rust, and the new production version written in Zig. +This repo contains the source code for the Roc compiler and related tools. There is the original prototype written in Rust (crates folder), and the new version written in Zig. -All discussion, unless otherwise explicitly requested, is regarding the new Zig production version. Therefore, ONLY files under the `src/` directory should be considered moving forward. +All discussion, unless otherwise explicitly requested, is regarding the new version in zig. Ignore everything in the crates folder at the root of the repo, always exclude it from searches. ## Documentation @@ -19,12 +19,15 @@ Roc uses two different test strategies to Verify Correctness, and Validate Behav 1. **Verify Correctness:** Where necessary, we add unit tests to ensure low-level or specific implementation details remain correct. These are written in `.zig` files using the `test` keyword alongside the code they are testing. These tests are typically limited in scope to a single function. 2. **Validate Behaviour:** More commonly, we add snapshot tests to provide assurance that the compiler continues to behave as expected. Snapshot files `.md` concurrently exercise many parts of the compiler by presenting the output of each stage for a given snippet of Roc code. Unlike unit tests, this has relevant debug-level information depth and multiple-compiler stage breadth. -### Usage +When you are requested to add a test without a specific location, take some time to look around and find the best location for the test in the repo. -- **Run Unit Tests** `zig build test` (note `zig test` doesn't work as we have a complicated build system, therefore we use `zig build test` also for individual tests). -- **Generate or Update All Snapshots Files** `zig build snapshot` -- **Generate or Update Specific Snapshot File** `zig build snapshot -- ` -- **Update EXPECTED from PROBLEMS in Snapshot File** `zig build update-expected -- ` +### Common Commands + +- Run all tests: `zig build test`. To run a specific test: `zig build test -- --test-filter "name of test"`. NEVER use the standard `zig test`, it does not work with our project. +- Generate or update all snapshots files: `zig build snapshot` +- Generate or update specific snapshot file: `zig build snapshot -- ` +- Update EXPECTED from PROBLEMS in snapshot file: `zig build update-expected -- ` +- To generate (or update) AND test the snapshots: `zig build snapshot && zig build test`. ### Snapshot File Structure diff --git a/BUILDING_FROM_SOURCE.md b/BUILDING_FROM_SOURCE.md index 382a8c24a1..b4436cc5a2 100644 --- a/BUILDING_FROM_SOURCE.md +++ b/BUILDING_FROM_SOURCE.md @@ -6,7 +6,7 @@ If you run into any problems getting Roc built from source, please ask for help ## Recommended way -[Download zig 0.14.1](https://ziglang.org/download/) and add it to your PATH. +[Download zig 0.15.2](https://ziglang.org/download/) and add it to your PATH. [Search "Setting up PATH"](https://ziglang.org/learn/getting-started/) for more details. Do a test run with diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 498835b39f..1b4b7006fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,6 +33,11 @@ zig build test zig build fmt ``` +To run a specific test: +```sh +zig build test -- --test-filter "name of test" +``` + ## Contribution Tips - If you've never made a pull request on github before, [this](https://www.freecodecamp.org/news/how-to-make-your-first-pull-request-on-github-3/) will be a good place to start. diff --git a/Glossary.md b/Glossary.md index dbb70971bd..55d6d4dd17 100644 --- a/Glossary.md +++ b/Glossary.md @@ -522,6 +522,50 @@ Another approach, manual memory management, would allow you to produce the faste Reference counting implementation: - Old compiler: [Mono folder](crates/compiler/mono/src) (search ref) +## Borrow + +A **borrowing** function reads its argument without affecting its reference count. +The caller retains ownership and can continue using the value after the call. + +Example builtins that borrow: `strEqual`, `listLen`, `strContains` + +See [src/builtins/OWNERSHIP.md](src/builtins/OWNERSHIP.md) for detailed ownership semantics. + +## Consume + +A **consuming** function takes ownership of its argument. The caller transfers +ownership to the callee and must not use the argument after the call. The function +is responsible for cleanup (decref when done). + +Example builtins that consume: `strConcat`, `listConcat`, `strJoinWith` + +See [src/builtins/OWNERSHIP.md](src/builtins/OWNERSHIP.md) for detailed ownership semantics. + +## Copy-on-Write + +A variant of consuming where the function may return the same allocation if the +input is unique (reference count == 1). If the input is shared, the function +decrefs the original and allocates a new copy. + +Example builtins: `strWithAsciiUppercased`, `strTrim`, `listAppend` + +## Seamless Slice + +A memory optimization where the result shares underlying data with the input +via a slice that holds a reference to the original allocation. + +There are two variants: + +1. **Borrowing seamless slice**: The builtin borrows the input and calls `incref` + to share the allocation. The interpreter should decref the input after the call. + Example: `strToUtf8` + +2. **Consuming seamless slice**: The builtin consumes the input and the slice + inherits the reference (no incref). The interpreter should NOT decref. + Example: `strTrim` (when it creates an offset slice) + +See [src/builtins/OWNERSHIP.md](src/builtins/OWNERSHIP.md) for detailed ownership semantics. + ## Mutate in place TODO diff --git a/build.zig b/build.zig index 54c40844de..054415da77 100644 --- a/build.zig +++ b/build.zig @@ -1,6 +1,8 @@ const std = @import("std"); const builtin = @import("builtin"); const modules = @import("src/build/modules.zig"); +const glibc_stub_build = @import("src/build/glibc_stub.zig"); +const roc_target = @import("src/target/mod.zig"); const Dependency = std.Build.Dependency; const Import = std.Build.Module.Import; const InstallDir = std.Build.InstallDir; @@ -9,29 +11,1699 @@ const OptimizeMode = std.builtin.OptimizeMode; const ResolvedTarget = std.Build.ResolvedTarget; const Step = std.Build.Step; +// Cross-compile target definitions + +/// Cross-compile target specification +const CrossTarget = struct { + name: []const u8, + query: std.Target.Query, +}; + +/// Musl-only cross-compile targets (static linking) +const musl_cross_targets = [_]CrossTarget{ + .{ .name = "x64musl", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl } }, + .{ .name = "arm64musl", .query = .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl } }, +}; + +/// Glibc cross-compile targets (dynamic linking) +const glibc_cross_targets = [_]CrossTarget{ + .{ .name = "x64glibc", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu } }, + .{ .name = "arm64glibc", .query = .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .gnu } }, +}; + +/// Windows cross-compile targets +const windows_cross_targets = [_]CrossTarget{ + .{ .name = "x64win", .query = .{ .cpu_arch = .x86_64, .os_tag = .windows, .abi = .msvc } }, + .{ .name = "arm64win", .query = .{ .cpu_arch = .aarch64, .os_tag = .windows, .abi = .msvc } }, +}; + +/// All Linux cross-compile targets (musl + glibc) +const linux_cross_targets = musl_cross_targets ++ glibc_cross_targets; + +/// Test platform directories that need host libraries built +const all_test_platform_dirs = [_][]const u8{ "str", "int", "fx", "fx-open" }; + +fn mustUseLlvm(target: ResolvedTarget) bool { + return target.result.os.tag == .macos and target.result.cpu.arch == .x86_64; +} + +fn configureBackend(step: *Step.Compile, target: ResolvedTarget) void { + if (mustUseLlvm(target)) { + step.use_llvm = true; + } +} + +fn isNativeishOrMusl(target: ResolvedTarget) bool { + return target.result.cpu.arch == builtin.target.cpu.arch and + target.query.isNativeOs() and + (target.query.isNativeAbi() or target.result.abi.isMusl()); +} + +const TestsSummaryStep = struct { + step: Step, + has_filters: bool, + forced_passes: u64, + + fn create( + b: *std.Build, + test_filters: []const []const u8, + forced_passes: usize, + ) *TestsSummaryStep { + const self = b.allocator.create(TestsSummaryStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.{ + .id = Step.Id.custom, + .name = "tests_summary", + .owner = b, + .makeFn = make, + }), + .has_filters = test_filters.len > 0, + .forced_passes = @intCast(forced_passes), + }; + return self; + } + + fn addRun(self: *TestsSummaryStep, run_step: *Step) void { + self.step.dependOn(run_step); + } + + fn make(step: *Step, options: Step.MakeOptions) !void { + _ = options; + + const self: *TestsSummaryStep = @fieldParentPtr("step", step); + + var passed: u64 = 0; + + for (step.dependencies.items) |dependency| { + const module_pass_count = dependency.test_results.passCount(); + passed += @intCast(module_pass_count); + } + + var effective_passed = passed; + if (self.has_filters and self.forced_passes != 0) { + const subtract = @min(effective_passed, self.forced_passes); + effective_passed -= subtract; + } + + if (effective_passed == 0) { + std.debug.print("No tests ran (all tests filtered out).\n", .{}); + } else { + std.debug.print("All {d} tests passed.\n", .{effective_passed}); + } + } +}; + +/// Build step that checks for forbidden patterns in the type checker code. +/// +/// During type checking, we NEVER do string or byte comparisons because: +/// 1. They take linear time, which can cause performance issues +/// 2. They are brittle to changes that type-checking should not be sensitive to +/// +/// Instead, we always compare indices - either into node stores or to interned string indices. +/// This step enforces that rule by failing the build if `std.mem.` is found in src/canonicalize/, src/check/, src/layout/, or src/eval/. +const CheckTypeCheckerPatternsStep = struct { + step: Step, + + fn create(b: *std.Build) *CheckTypeCheckerPatternsStep { + const self = b.allocator.create(CheckTypeCheckerPatternsStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.{ + .id = Step.Id.custom, + .name = "check-type-checker-patterns", + .owner = b, + .makeFn = make, + }), + }; + return self; + } + + fn make(step: *Step, _: Step.MakeOptions) !void { + const b = step.owner; + const allocator = b.allocator; + + var violations = std.ArrayList(Violation).empty; + defer violations.deinit(allocator); + + // Recursively scan src/canonicalize/, src/check/, src/layout/, and src/eval/ for .zig files + // TODO: uncomment "src/canonicalize" once its std.mem violations are fixed + const dirs_to_scan = [_][]const u8{ "src/check", "src/layout", "src/eval" }; + for (dirs_to_scan) |dir_path| { + var dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch |err| { + return step.fail("Failed to open {s} directory: {}", .{ dir_path, err }); + }; + defer dir.close(); + + try scanDirectory(allocator, dir, dir_path, &violations); + } + + if (violations.items.len > 0) { + std.debug.print("\n", .{}); + std.debug.print("=" ** 80 ++ "\n", .{}); + std.debug.print("FORBIDDEN PATTERN DETECTED\n", .{}); + std.debug.print("=" ** 80 ++ "\n\n", .{}); + + std.debug.print( + \\Code in src/canonicalize/, src/check/, src/layout/, and src/eval/ must NOT do raw string comparison or manipulation. + \\ + \\WHY THIS RULE EXISTS: + \\ We NEVER do string or byte comparisons because: + \\ + \\ 1. PERFORMANCE: String comparisons take O(n) time where n is the string + \\ length. These code paths can involve many comparisons, so this adds up. + \\ + \\ 2. BRITTLENESS: String comparisons make the code sensitive to changes it + \\ shouldn't care about (e.g., how identifiers are rendered, whitespace, + \\ formatting). This leads to subtle bugs. + \\ + \\WHAT TO DO INSTEAD: + \\ Always compare indices rather than strings: + \\ + \\ - For identifiers: Compare Ident.Idx values (interned string indices) + \\ - For types: Compare type variable indices or node store indices + \\ - For expressions: Compare Expr.Idx values from the node store + \\ + \\ Example - WRONG: + \\ if (std.mem.eql(u8, ident_name, "is_eq")) {{ ... }} + \\ + \\ Example - RIGHT: + \\ if (ident_idx == module_env.idents.is_eq) {{ ... }} + \\ + \\VIOLATIONS FOUND: + \\ + , .{}); + + for (violations.items) |violation| { + std.debug.print(" {s}:{d}: {s}\n", .{ + violation.file_path, + violation.line_number, + violation.line_content, + }); + } + + std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{}); + + return step.fail( + "Found {d} forbidden patterns (raw string comparison or manipulation) in src/canonicalize/, src/check/, src/layout/, or src/eval/. " ++ + "See above for details on why this is forbidden and what to do instead.", + .{violations.items.len}, + ); + } + } + + const Violation = struct { + file_path: []const u8, + line_number: usize, + line_content: []const u8, + }; + + fn scanDirectory( + allocator: std.mem.Allocator, + dir: std.fs.Dir, + path_prefix: []const u8, + violations: *std.ArrayList(Violation), + ) !void { + var walker = try dir.walk(allocator); + defer walker.deinit(); + + while (try walker.next()) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.path, ".zig")) continue; + + // Skip test files - they may legitimately need string comparison for assertions + if (std.mem.endsWith(u8, entry.path, "_test.zig")) continue; + if (std.mem.indexOf(u8, entry.path, "test/") != null) continue; + if (std.mem.startsWith(u8, entry.path, "test")) continue; + if (std.mem.endsWith(u8, entry.path, "test_runner.zig")) continue; + + const full_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ path_prefix, entry.path }); + + const file = dir.openFile(entry.path, .{}) catch continue; + defer file.close(); + + const content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch continue; + defer allocator.free(content); + + var line_number: usize = 1; + var line_start: usize = 0; + + for (content, 0..) |char, i| { + if (char == '\n') { + const line = content[line_start..i]; + + const trimmed = std.mem.trim(u8, line, " \t"); + // Skip comments + if (std.mem.startsWith(u8, trimmed, "//")) { + line_number += 1; + line_start = i + 1; + continue; + } + + // Check for std.mem. usage (but allow safe patterns) + if (std.mem.indexOf(u8, line, "std.mem.")) |idx| { + const after_match = line[idx + 8 ..]; + + // Allow these safe patterns that don't involve string/byte comparison: + // - std.mem.Allocator: a type, not a comparison + // - std.mem.Alignment: a type, not a comparison + // - std.mem.sort: sorting by custom comparator, not string comparison + // - std.mem.asBytes: type punning, not string comparison + // - std.mem.reverse: reversing arrays, not string comparison + // - std.mem.alignForward: memory alignment arithmetic, not string comparison + // - std.mem.order: sort ordering (used by sort comparators), not string comparison + // - std.mem.copyForwards: byte copying, not string comparison + const is_allowed = + std.mem.startsWith(u8, after_match, "Allocator") or + std.mem.startsWith(u8, after_match, "Alignment") or + std.mem.startsWith(u8, after_match, "sort") or + std.mem.startsWith(u8, after_match, "asBytes") or + std.mem.startsWith(u8, after_match, "reverse") or + std.mem.startsWith(u8, after_match, "alignForward") or + std.mem.startsWith(u8, after_match, "order") or + std.mem.startsWith(u8, after_match, "copyForwards"); + + if (!is_allowed) { + try violations.append(allocator, .{ + .file_path = full_path, + .line_number = line_number, + .line_content = try allocator.dupe(u8, trimmed), + }); + } + } + + // Check for findByString usage - should use Ident.Idx comparison instead + if (std.mem.indexOf(u8, line, "findByString") != null) { + try violations.append(allocator, .{ + .file_path = full_path, + .line_number = line_number, + .line_content = try allocator.dupe(u8, trimmed), + }); + } + + // Check for findIdent usage - should use pre-stored Ident.Idx instead + if (std.mem.indexOf(u8, line, "findIdent") != null) { + try violations.append(allocator, .{ + .file_path = full_path, + .line_number = line_number, + .line_content = try allocator.dupe(u8, trimmed), + }); + } + + // Check for getMethodIdent usage - should use pre-stored Ident.Idx instead + if (std.mem.indexOf(u8, line, "getMethodIdent") != null) { + try violations.append(allocator, .{ + .file_path = full_path, + .line_number = line_number, + .line_content = try allocator.dupe(u8, trimmed), + }); + } + + line_number += 1; + line_start = i + 1; + } + } + } + } +}; + +/// Build step that checks for @enumFromInt(0) usage in all .zig files. +/// +/// We forbid @enumFromInt(0) because it hides bugs and makes them harder to debug. +/// If we need a placeholder value that we believe will never be read, we should +/// use `undefined` instead - that way our intent is clear, and it can fail in a +/// more obvious way if our assumption is incorrect. +const CheckEnumFromIntZeroStep = struct { + step: Step, + + fn create(b: *std.Build) *CheckEnumFromIntZeroStep { + const self = b.allocator.create(CheckEnumFromIntZeroStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.{ + .id = Step.Id.custom, + .name = "check-enum-from-int-zero", + .owner = b, + .makeFn = make, + }), + }; + return self; + } + + fn make(step: *Step, options: Step.MakeOptions) !void { + _ = options; + const b = step.owner; + const allocator = b.allocator; + + var violations = std.ArrayList(Violation).empty; + defer violations.deinit(allocator); + + // Recursively scan src/ for .zig files + var dir = std.fs.cwd().openDir("src", .{ .iterate = true }) catch |err| { + return step.fail("Failed to open src directory: {}", .{err}); + }; + defer dir.close(); + + try scanDirectoryForEnumFromIntZero(allocator, dir, "src", &violations); + + if (violations.items.len > 0) { + std.debug.print("\n", .{}); + std.debug.print("=" ** 80 ++ "\n", .{}); + std.debug.print("FORBIDDEN PATTERN: @enumFromInt(0)\n", .{}); + std.debug.print("=" ** 80 ++ "\n\n", .{}); + + std.debug.print( + \\Using @enumFromInt(0) is forbidden in this codebase. + \\ + \\WHY THIS RULE EXISTS: + \\ @enumFromInt(0) hides bugs and makes them harder to debug. It creates + \\ a "valid-looking" value that can silently propagate through the code + \\ when something goes wrong. + \\ + \\WHAT TO DO INSTEAD: + \\ If you need a placeholder value that you believe will never be read, + \\ use `undefined` instead. This makes your intent clear, and if your + \\ assumption is wrong and the value IS read, it will fail more obviously. + \\ + \\ When using `undefined`, add a comment explaining why it's correct there + \\ (e.g., where it will be overwritten before being read). + \\ + \\ Example - WRONG: + \\ .anno = @enumFromInt(0), // placeholder - will be replaced + \\ + \\ Example - RIGHT: + \\ .anno = undefined, // overwritten in Phase 1.7 before use + \\ + \\VIOLATIONS FOUND: + \\ + , .{}); + + for (violations.items) |violation| { + std.debug.print(" {s}:{d}: {s}\n", .{ + violation.file_path, + violation.line_number, + violation.line_content, + }); + } + + std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{}); + + return step.fail( + "Found {d} uses of @enumFromInt(0). Using placeholder values like this has consistently led to bugs in this code base. " ++ + "Do not use @enumFromInt(0) and also do not uncritically replace it with another placeholder like .first or something like that. " ++ + "If you want it to be uninitialized and are very confident it will be overwritten before it is ever read, then use `undefined`. " ++ + "Otherwise, take a step back and rethink how this code works; there should be a way to implement this in a way that does not use hardcoded placeholder indices like 0! " ++ + "See above for details.", + .{violations.items.len}, + ); + } + } + + const Violation = struct { + file_path: []const u8, + line_number: usize, + line_content: []const u8, + }; + + fn scanDirectoryForEnumFromIntZero( + allocator: std.mem.Allocator, + dir: std.fs.Dir, + path_prefix: []const u8, + violations: *std.ArrayList(Violation), + ) !void { + var walker = try dir.walk(allocator); + defer walker.deinit(); + + while (try walker.next()) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.path, ".zig")) continue; + + const full_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ path_prefix, entry.path }); + + const file = dir.openFile(entry.path, .{}) catch continue; + defer file.close(); + + const content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch continue; + defer allocator.free(content); + + var line_number: usize = 1; + var line_start: usize = 0; + + for (content, 0..) |char, i| { + if (char == '\n') { + const line = content[line_start..i]; + + const trimmed = std.mem.trim(u8, line, " \t"); + // Skip comments + if (std.mem.startsWith(u8, trimmed, "//")) { + line_number += 1; + line_start = i + 1; + continue; + } + + // Check for @enumFromInt(0) usage + if (std.mem.indexOf(u8, line, "@enumFromInt(0)") != null) { + try violations.append(allocator, .{ + .file_path = full_path, + .line_number = line_number, + .line_content = try allocator.dupe(u8, trimmed), + }); + } + + line_number += 1; + line_start = i + 1; + } + } + } + } +}; + +/// Build step that checks for unused variable suppression patterns. +/// +/// In this codebase, we don't use `_ = variable;` to suppress unused variable warnings. +/// Instead, we delete the unused variable/argument and update all call sites as necessary. +const CheckUnusedSuppressionStep = struct { + step: Step, + + fn create(b: *std.Build) *CheckUnusedSuppressionStep { + const self = b.allocator.create(CheckUnusedSuppressionStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.{ + .id = Step.Id.custom, + .name = "check-unused-suppression", + .owner = b, + .makeFn = make, + }), + }; + return self; + } + + fn make(step: *Step, _: Step.MakeOptions) !void { + const b = step.owner; + const allocator = b.allocator; + + var violations = std.ArrayList(Violation).empty; + defer violations.deinit(allocator); + + // Scan all src/ directories for .zig files + var dir = std.fs.cwd().openDir("src", .{ .iterate = true }) catch |err| { + return step.fail("Failed to open src/ directory: {}", .{err}); + }; + defer dir.close(); + + try scanDirectoryForUnusedSuppression(allocator, dir, "src", &violations); + + if (violations.items.len > 0) { + std.debug.print("\n", .{}); + std.debug.print("=" ** 80 ++ "\n", .{}); + std.debug.print("UNUSED VARIABLE SUPPRESSION DETECTED\n", .{}); + std.debug.print("=" ** 80 ++ "\n\n", .{}); + + std.debug.print( + \\In this codebase, we do NOT use `_ = variable;` to suppress unused warnings. + \\ + \\Instead, you should: + \\ 1. Delete the unused variable, parameter, or argument + \\ 2. Update all call sites as necessary + \\ 3. Propagate the change through the codebase until tests pass + \\ + \\VIOLATIONS FOUND: + \\ + , .{}); + + for (violations.items) |violation| { + std.debug.print(" {s}:{d}: {s}\n", .{ + violation.file_path, + violation.line_number, + violation.line_content, + }); + } + + std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{}); + + return step.fail( + "Found {d} unused variable suppression patterns (`_ = identifier;`). " ++ + "Delete the unused variables and update call sites instead.", + .{violations.items.len}, + ); + } + } + + const Violation = struct { + file_path: []const u8, + line_number: usize, + line_content: []const u8, + }; + + fn scanDirectoryForUnusedSuppression( + allocator: std.mem.Allocator, + dir: std.fs.Dir, + path_prefix: []const u8, + violations: *std.ArrayList(Violation), + ) !void { + var walker = try dir.walk(allocator); + defer walker.deinit(); + + while (try walker.next()) |entry| { + if (entry.kind != .file) continue; + if (!std.mem.endsWith(u8, entry.path, ".zig")) continue; + + const full_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ path_prefix, entry.path }); + + const file = dir.openFile(entry.path, .{}) catch continue; + defer file.close(); + + const content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch continue; + defer allocator.free(content); + + var line_number: usize = 1; + var line_start: usize = 0; + + for (content, 0..) |char, i| { + if (char == '\n') { + const line = content[line_start..i]; + const trimmed = std.mem.trim(u8, line, " \t"); + + // Check for pattern: _ = identifier; + // where identifier is alphanumeric with underscores + if (isUnusedSuppression(trimmed)) { + try violations.append(allocator, .{ + .file_path = full_path, + .line_number = line_number, + .line_content = try allocator.dupe(u8, trimmed), + }); + } + + line_number += 1; + line_start = i + 1; + } + } + } + } + + fn isUnusedSuppression(line: []const u8) bool { + // Pattern: `_ = identifier;` where identifier is alphanumeric with underscores + // Must start with "_ = " and end with ";" + if (!std.mem.startsWith(u8, line, "_ = ")) return false; + if (!std.mem.endsWith(u8, line, ";")) return false; + + // Extract the identifier part (between "_ = " and ";") + const identifier = line[4 .. line.len - 1]; + + // Must have at least one character + if (identifier.len == 0) return false; + + // Check that identifier contains only alphanumeric chars and underscores + // Also allow dots for field access like `_ = self.field;` which we also want to catch + for (identifier) |c| { + if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '.') { + return false; + } + } + + return true; + } +}; + +/// Build step that checks for @panic and std.debug.panic usage in interpreter and builtins. +/// +/// In Roc's design philosophy, compile-time errors become runtime errors with helpful messages. +/// Users can run apps despite errors, and we provide actionable feedback. Using @panic unwinds +/// the stack and prevents showing helpful error messages. +/// +/// Additionally, in WASM builds, @panic compiles to the `unreachable` instruction with no +/// message output, making debugging impossible. All runtime code must use roc_ops.crash() +/// to ensure error messages are properly displayed. +const CheckPanicStep = struct { + step: Step, + + // Files to scan individually + const scan_files = [_][]const u8{ + "src/eval/interpreter.zig", + "src/eval/StackValue.zig", + }; + + // Directories to scan (all .zig files within) + const scan_dirs = [_][]const u8{ + "src/builtins", + }; + + // Files to exclude from scanning (test-only files) + const excluded_files = [_][]const u8{ + "fuzz_sort.zig", + }; + + // Line-level allowlist patterns - if any of these appear on the line, allow the @panic + const allowlist_patterns = [_][]const u8{ + "trace_modules", // traceDbg helper in interpreter + }; + + // File-specific line ranges to exclude (test-only code) + // Format: { file_suffix, start_line, end_line } + const ExcludedRange = struct { file: []const u8, start: usize, end: usize }; + const excluded_ranges = [_]ExcludedRange{ + // TestEnv struct in utils.zig is test-only (lines 60-214) + .{ .file = "utils.zig", .start = 60, .end = 214 }, + }; + + fn create(b: *std.Build) *CheckPanicStep { + const self = b.allocator.create(CheckPanicStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.{ + .id = Step.Id.custom, + .name = "check-panic-usage", + .owner = b, + .makeFn = makePanic, + }), + }; + return self; + } + + fn isExcludedFile(file_name: []const u8) bool { + for (excluded_files) |excluded| { + if (std.mem.eql(u8, file_name, excluded)) return true; + } + return false; + } + + fn isAllowlisted(line: []const u8) bool { + for (allowlist_patterns) |pattern| { + if (std.mem.indexOf(u8, line, pattern) != null) return true; + } + return false; + } + + fn isInExcludedRange(file_path: []const u8, line_number: usize) bool { + for (excluded_ranges) |range| { + if (std.mem.endsWith(u8, file_path, range.file)) { + if (line_number >= range.start and line_number <= range.end) { + return true; + } + } + } + return false; + } + + fn scanFile(allocator: std.mem.Allocator, file_path: []const u8, violations: *std.ArrayList(Violation)) !void { + const file = std.fs.cwd().openFile(file_path, .{}) catch |err| { + std.debug.print("Warning: Failed to open {s}: {}\n", .{ file_path, err }); + return; + }; + defer file.close(); + + const content = file.readToEndAlloc(allocator, 50 * 1024 * 1024) catch |err| { + std.debug.print("Warning: Failed to read {s}: {}\n", .{ file_path, err }); + return; + }; + defer allocator.free(content); + + var line_number: usize = 1; + var line_start: usize = 0; + + for (content, 0..) |char, i| { + if (char == '\n') { + const line = content[line_start..i]; + const trimmed = std.mem.trim(u8, line, " \t"); + + // Skip comments + if (!std.mem.startsWith(u8, trimmed, "//")) { + // Check for @panic usage + const has_panic = std.mem.indexOf(u8, line, "@panic(") != null; + // Check for std.debug.panic usage + const has_debug_panic = std.mem.indexOf(u8, line, "std.debug.panic") != null; + + if (has_panic or has_debug_panic) { + if (!isAllowlisted(line) and !isInExcludedRange(file_path, line_number)) { + try violations.append(allocator, .{ + .file_path = try allocator.dupe(u8, file_path), + .line_number = line_number, + .line_content = try allocator.dupe(u8, trimmed), + }); + } + } + } + + line_number += 1; + line_start = i + 1; + } + } + } + + fn makePanic(step: *Step, _: Step.MakeOptions) !void { + const b = step.owner; + const allocator = b.allocator; + + var violations = std.ArrayList(Violation).empty; + defer violations.deinit(allocator); + + // Scan individual files + for (scan_files) |file_path| { + try scanFile(allocator, file_path, &violations); + } + + // Scan directories + for (scan_dirs) |dir_path| { + var dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch |err| { + std.debug.print("Warning: Failed to open directory {s}: {}\n", .{ dir_path, err }); + continue; + }; + defer dir.close(); + + var iter = dir.iterate(); + while (try iter.next()) |entry| { + if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".zig")) { + if (!isExcludedFile(entry.name)) { + const full_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ dir_path, entry.name }); + defer allocator.free(full_path); + try scanFile(allocator, full_path, &violations); + } + } + } + } + + if (violations.items.len > 0) { + std.debug.print("\n", .{}); + std.debug.print("=" ** 80 ++ "\n", .{}); + std.debug.print("FORBIDDEN PATTERN: @panic / std.debug.panic in runtime code\n", .{}); + std.debug.print("=" ** 80 ++ "\n\n", .{}); + + std.debug.print( + \\Using @panic or std.debug.panic is forbidden in interpreter and builtins. + \\ + \\WHY THIS RULE EXISTS: + \\ 1. Roc's design philosophy is that compile-time errors become runtime errors with + \\ helpful messages. Users can run apps despite errors, and we provide actionable + \\ feedback. @panic unwinds the stack and prevents us from showing helpful errors. + \\ + \\ 2. In WASM builds, @panic compiles to the `unreachable` instruction with NO + \\ message output, making debugging impossible. + \\ + \\WHAT TO DO INSTEAD: + \\ In interpreter.zig, use the triggerCrash() method: + \\ + \\ self.triggerCrash("Description of the error", false, roc_ops); + \\ + \\ In StackValue.zig and builtins, use roc_ops.crash(): + \\ + \\ roc_ops.crash("Description of the error"); + \\ + \\ For debug output, use roc_ops.dbg(): + \\ + \\ roc_ops.dbg("Debug message"); + \\ + \\VIOLATIONS FOUND: + \\ + , .{}); + + for (violations.items) |violation| { + std.debug.print(" {s}:{d}: {s}\n", .{ + violation.file_path, + violation.line_number, + violation.line_content, + }); + } + + std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{}); + + return step.fail( + "Found {d} uses of @panic or std.debug.panic in runtime code. " ++ + "Use roc_ops.crash() to report errors through the proper RocOps crash handler. " ++ + "See above for details.", + .{violations.items.len}, + ); + } + } + + const Violation = struct { + file_path: []const u8, + line_number: usize, + line_content: []const u8, + }; +}; + +/// Build step that checks for global stdio usage in CLI code. +/// +/// The CLI code uses a context-based I/O pattern where stdout/stderr are accessed +/// through `ctx.io.stdout()` and `ctx.io.stderr()`. This prepares for Zig's upcoming +/// I/O interface changes where I/O is passed through functions (like Allocator). +/// +/// This step enforces that pattern by failing the build if direct global stdio +/// access is found in src/cli/main.zig. +const CheckCliGlobalStdioStep = struct { + step: Step, + + fn create(b: *std.Build) *CheckCliGlobalStdioStep { + const self = b.allocator.create(CheckCliGlobalStdioStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.{ + .id = Step.Id.custom, + .name = "check-cli-global-stdio", + .owner = b, + .makeFn = make, + }), + }; + return self; + } + + fn make(step: *Step, _: Step.MakeOptions) !void { + const b = step.owner; + const allocator = b.allocator; + + var violations = std.ArrayList(Violation).empty; + defer violations.deinit(allocator); + + // Only scan src/cli/main.zig + const file_path = "src/cli/main.zig"; + const file = std.fs.cwd().openFile(file_path, .{}) catch |err| { + return step.fail("Failed to open {s}: {}", .{ file_path, err }); + }; + defer file.close(); + + const content = file.readToEndAlloc(allocator, 10 * 1024 * 1024) catch |err| { + return step.fail("Failed to read {s}: {}", .{ file_path, err }); + }; + defer allocator.free(content); + + var line_number: usize = 1; + var line_start: usize = 0; + + for (content, 0..) |char, i| { + if (char == '\n') { + const line = content[line_start..i]; + const trimmed = std.mem.trim(u8, line, " \t"); + + // Check for forbidden patterns that indicate global stdio usage + // These patterns bypass ctx.io and use global state + const forbidden_patterns = [_][]const u8{ + "std.io.getStdOut()", + "std.io.getStdErr()", + "std.fs.File.stdout()", + "std.fs.File.stderr()", + }; + + for (forbidden_patterns) |pattern| { + if (std.mem.indexOf(u8, trimmed, pattern) != null) { + try violations.append(allocator, .{ + .file_path = file_path, + .line_number = line_number, + .line_content = try allocator.dupe(u8, trimmed), + .pattern = pattern, + }); + } + } + + line_number += 1; + line_start = i + 1; + } + } + + if (violations.items.len > 0) { + std.debug.print("\n", .{}); + std.debug.print("=" ** 80 ++ "\n", .{}); + std.debug.print("GLOBAL STDIO USAGE DETECTED IN CLI\n", .{}); + std.debug.print("=" ** 80 ++ "\n\n", .{}); + + std.debug.print( + \\In the CLI code, we use context-based I/O, not global stdio functions. + \\ + \\WHY THIS RULE EXISTS: + \\ 1. TESTABILITY: Context-based I/O allows tests to inject mock writers + \\ to capture and verify output. + \\ + \\ 2. FUTURE COMPATIBILITY: Zig's upcoming I/O interface will pass I/O + \\ through functions (like Allocator). Using ctx.io prepares us for this. + \\ + \\ 3. CONSISTENCY: All CLI functions receive ctx which contains allocators + \\ and I/O. This provides a uniform interface for resources. + \\ + \\WHAT TO DO INSTEAD: + \\ Access stdout/stderr through the CliContext: + \\ + \\ Example - WRONG: + \\ const stdout = std.io.getStdOut().writer(); + \\ const stderr = std.fs.File.stderr().writer(); + \\ + \\ Example - RIGHT: + \\ const stdout = ctx.io.stdout(); + \\ const stderr = ctx.io.stderr(); + \\ + \\VIOLATIONS FOUND: + \\ + , .{}); + + for (violations.items) |violation| { + std.debug.print(" {s}:{d}: found `{s}` in: {s}\n", .{ + violation.file_path, + violation.line_number, + violation.pattern, + violation.line_content, + }); + } + + std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{}); + + return step.fail( + "Found {d} global stdio usage(s) in CLI code. " ++ + "Use ctx.io.stdout() and ctx.io.stderr() instead.", + .{violations.items.len}, + ); + } + } + + const Violation = struct { + file_path: []const u8, + line_number: usize, + line_content: []const u8, + pattern: []const u8, + }; +}; + +fn checkFxPlatformTestCoverage(step: *Step) !void { + const b = step.owner; + std.debug.print("---- checking fx platform test coverage ----\n", .{}); + + const allocator = b.allocator; + + // Get all .roc files in test/fx (excluding subdirectories) + var fx_dir = try std.fs.cwd().openDir("test/fx", .{ .iterate = true }); + defer fx_dir.close(); + + var roc_files = std.ArrayList([]const u8).empty; + defer { + for (roc_files.items) |file| { + allocator.free(file); + } + roc_files.deinit(allocator); + } + + var dir_iter = fx_dir.iterate(); + while (try dir_iter.next()) |entry| { + if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".roc")) { + const file_name = try allocator.dupe(u8, entry.name); + try roc_files.append(allocator, file_name); + } + } + + // Sort the list for consistent output + std.mem.sort([]const u8, roc_files.items, {}, struct { + fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool { + return std.mem.order(u8, lhs, rhs) == .lt; + } + }.lessThan); + + // Find all references to test/fx/*.roc files in test source files + var tested_files = std.StringHashMap(void).init(allocator); + defer { + var key_iter = tested_files.keyIterator(); + while (key_iter.next()) |key| { + allocator.free(key.*); + } + tested_files.deinit(); + } + + // Scan both the test file and the shared specs file + const test_files_to_scan = [_][]const u8{ + "src/cli/test/fx_platform_test.zig", + "src/cli/test/fx_test_specs.zig", + }; + + for (test_files_to_scan) |test_file_path| { + const test_file_contents = std.fs.cwd().readFileAlloc(allocator, test_file_path, 1024 * 1024) catch |err| { + std.debug.print("Warning: Could not read {s}: {}\n", .{ test_file_path, err }); + continue; + }; + defer allocator.free(test_file_contents); + + var line_iter = std.mem.splitScalar(u8, test_file_contents, '\n'); + while (line_iter.next()) |line| { + // Look for patterns like "test/fx/filename.roc" + var search_start: usize = 0; + while (std.mem.indexOfPos(u8, line, search_start, "test/fx/")) |idx| { + const rest_of_line = line[idx..]; + // Find the end of the filename + if (std.mem.indexOf(u8, rest_of_line, ".roc")) |roc_pos| { + const full_path = rest_of_line[0 .. roc_pos + 4]; // Include ".roc" + // Extract just the filename (after "test/fx/") + const filename = full_path["test/fx/".len..]; + // Only count files in test/fx (not subdirectories like test/fx/subdir/) + if (std.mem.indexOf(u8, filename, "/") == null) { + // Dupe the filename since the source buffer will be freed + const duped_filename = try allocator.dupe(u8, filename); + try tested_files.put(duped_filename, {}); + } + } + search_start = idx + 1; + } + } + } + + // Find missing tests + var missing_tests = std.ArrayList([]const u8).empty; + defer missing_tests.deinit(allocator); + + for (roc_files.items) |roc_file| { + if (!tested_files.contains(roc_file)) { + try missing_tests.append(allocator, roc_file); + } + } + + // Report results + if (missing_tests.items.len > 0) { + std.debug.print("\nERROR: The following .roc files in test/fx/ do not have tests:\n", .{}); + for (missing_tests.items) |missing_file| { + std.debug.print(" - {s}\n", .{missing_file}); + } + std.debug.print("\nPlease add tests in fx_platform_test.zig or fx_test_specs.zig, or remove these files from test/fx/.\n", .{}); + return step.fail("{d} .roc file(s) in test/fx/ are missing tests", .{missing_tests.items.len}); + } + + std.debug.print("All {d} .roc files in test/fx/ have tests.\n", .{roc_files.items.len}); +} + +const CheckFxStep = struct { + step: Step, + + fn create(b: *std.Build) *CheckFxStep { + const self = b.allocator.create(CheckFxStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.{ + .id = Step.Id.custom, + .name = "checkfx-inner", + .owner = b, + .makeFn = make, + }), + }; + return self; + } + + fn make(step: *Step, options: Step.MakeOptions) !void { + _ = options; + try checkFxPlatformTestCoverage(step); + } +}; + +const MiniCiStep = struct { + step: Step, + + fn create(b: *std.Build) *MiniCiStep { + const self = b.allocator.create(MiniCiStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.{ + .id = Step.Id.custom, + .name = "minici-inner", + .owner = b, + .makeFn = make, + }), + }; + return self; + } + + fn make(step: *Step, options: Step.MakeOptions) !void { + _ = options; + + // Run the sequence of `zig build` commands that make up the + // mini CI pipeline. + try runSubBuild(step, "fmt", "zig build fmt"); + try runZigLints(step); + try checkTestWiring(step); + try runSubBuild(step, null, "zig build"); + try checkBuiltinRocFormatting(step); + try runSubBuild(step, "snapshot", "zig build snapshot"); + try checkSnapshotChanges(step); + try checkFxPlatformTestCoverage(step); + try runSubBuild(step, "test", "zig build test"); + try runSubBuild(step, "test-playground", "zig build test-playground"); + try runSubBuild(step, "test-serialization-sizes", "zig build test-serialization-sizes"); + try runSubBuild(step, "test-cli", "zig build test-cli"); + } + + fn runZigLints(step: *Step) !void { + const b = step.owner; + std.debug.print("---- minici: running zig lints ----\n", .{}); + + var child_argv = std.ArrayList([]const u8).empty; + defer child_argv.deinit(b.allocator); + + try child_argv.append(b.allocator, b.graph.zig_exe); + try child_argv.append(b.allocator, "run"); + try child_argv.append(b.allocator, "ci/zig_lints.zig"); + + var child = std.process.Child.init(child_argv.items, b.allocator); + child.stdin_behavior = .Inherit; + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + + const term = try child.spawnAndWait(); + + switch (term) { + .Exited => |code| { + if (code != 0) { + return step.fail("Zig lints failed. Run 'zig run ci/zig_lints.zig' to see details.", .{}); + } + }, + else => { + return step.fail("zig run ci/zig_lints.zig terminated abnormally", .{}); + }, + } + } + + fn checkBuiltinRocFormatting(step: *Step) !void { + const b = step.owner; + std.debug.print("---- minici: checking Builtin.roc formatting ----\n", .{}); + + var child_argv = std.ArrayList([]const u8).empty; + defer child_argv.deinit(b.allocator); + + try child_argv.append(b.allocator, "./zig-out/bin/roc"); + try child_argv.append(b.allocator, "fmt"); + try child_argv.append(b.allocator, "--check"); + try child_argv.append(b.allocator, "src/build/roc/Builtin.roc"); + + var child = std.process.Child.init(child_argv.items, b.allocator); + child.stdin_behavior = .Inherit; + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + + const term = try child.spawnAndWait(); + + switch (term) { + .Exited => |code| { + if (code != 0) { + return step.fail( + "src/build/roc/Builtin.roc is not formatted. " ++ + "Run 'zig build run -- fmt src/build/roc/Builtin.roc' to format it.", + .{}, + ); + } + }, + else => { + return step.fail("roc fmt --check terminated abnormally", .{}); + }, + } + } + + fn checkSnapshotChanges(step: *Step) !void { + const b = step.owner; + std.debug.print("---- minici: checking for snapshot changes ----\n", .{}); + + var child_argv = std.ArrayList([]const u8).empty; + defer child_argv.deinit(b.allocator); + + try child_argv.append(b.allocator, "git"); + try child_argv.append(b.allocator, "diff"); + try child_argv.append(b.allocator, "--exit-code"); + try child_argv.append(b.allocator, "test/snapshots"); + + var child = std.process.Child.init(child_argv.items, b.allocator); + child.stdin_behavior = .Inherit; + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + + const term = try child.spawnAndWait(); + + switch (term) { + .Exited => |code| { + if (code != 0) { + return step.fail( + "Snapshots in 'test/snapshots' have changed. " ++ + "Run 'zig build snapshot' locally, review the updates, and commit the changes.", + .{}, + ); + } + }, + else => { + return step.fail("git diff terminated abnormally", .{}); + }, + } + } + + fn runSubBuild( + step: *Step, + step_name: ?[]const u8, + display: []const u8, + ) !void { + const b = step.owner; + std.debug.print("---- minici: running `{s}` ----\n", .{display}); + + var child_argv = std.ArrayList([]const u8).empty; + defer child_argv.deinit(b.allocator); + + // Build a clean zig build command for the requested step. + try child_argv.append(b.allocator, b.graph.zig_exe); // zig executable + try child_argv.append(b.allocator, "build"); + + if (step_name) |name| { + try child_argv.append(b.allocator, name); + } + + var child = std.process.Child.init(child_argv.items, b.allocator); + child.stdin_behavior = .Inherit; + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + + const term = try child.spawnAndWait(); + + switch (term) { + .Exited => |code| { + if (code != 0) { + return step.fail("`{s}` failed with exit code {d}", .{ display, code }); + } + }, + else => { + return step.fail("`{s}` terminated abnormally", .{display}); + }, + } + } + + fn checkTestWiring(step: *Step) !void { + const b = step.owner; + std.debug.print("---- minici: checking test wiring ----\n", .{}); + + var child_argv = std.ArrayList([]const u8).empty; + defer child_argv.deinit(b.allocator); + + try child_argv.append(b.allocator, b.graph.zig_exe); + try child_argv.append(b.allocator, "run"); + try child_argv.append(b.allocator, "ci/check_test_wiring.zig"); + + var child = std.process.Child.init(child_argv.items, b.allocator); + child.stdin_behavior = .Inherit; + child.stdout_behavior = .Inherit; + child.stderr_behavior = .Inherit; + + const term = try child.spawnAndWait(); + + switch (term) { + .Exited => |code| { + if (code != 0) { + return step.fail( + "Test wiring check failed. Run 'zig run ci/check_test_wiring.zig' to see details.", + .{}, + ); + } + }, + else => { + return step.fail("zig run ci/check_test_wiring.zig terminated abnormally", .{}); + }, + } + } +}; + +fn createAndRunBuiltinCompiler( + b: *std.Build, + roc_modules: modules.RocModules, + flag_enable_tracy: ?[]const u8, + roc_files: []const []const u8, +) *Step.Run { + // Build and run the compiler + const builtin_compiler_exe = b.addExecutable(.{ + .name = "builtin_compiler", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/build/builtin_compiler/main.zig"), + .target = b.graph.host, // this runs at build time on the *host* machine! + .optimize = .Debug, // No need to optimize - only compiles builtin modules + // Note: libc linking is handled by add_tracy below (required when tracy is enabled) + }), + }); + configureBackend(builtin_compiler_exe, b.graph.host); + + // Add only the minimal modules needed for parsing/checking + builtin_compiler_exe.root_module.addImport("base", roc_modules.base); + builtin_compiler_exe.root_module.addImport("collections", roc_modules.collections); + builtin_compiler_exe.root_module.addImport("types", roc_modules.types); + builtin_compiler_exe.root_module.addImport("parse", roc_modules.parse); + builtin_compiler_exe.root_module.addImport("can", roc_modules.can); + builtin_compiler_exe.root_module.addImport("check", roc_modules.check); + builtin_compiler_exe.root_module.addImport("reporting", roc_modules.reporting); + builtin_compiler_exe.root_module.addImport("builtins", roc_modules.builtins); + + // Add tracy support (required by parse/can/check modules) + add_tracy(b, roc_modules.build_options, builtin_compiler_exe, b.graph.host, false, flag_enable_tracy); + + // Run the builtin compiler to generate .bin files in zig-out/builtins/ + const run_builtin_compiler = b.addRunArtifact(builtin_compiler_exe); + + // Add all .roc files as explicit file inputs so Zig's cache tracks them + for (roc_files) |roc_path| { + run_builtin_compiler.addFileArg(b.path(roc_path)); + } + + return run_builtin_compiler; +} + +fn createTestPlatformHostLib( + b: *std.Build, + name: []const u8, + host_path: []const u8, + target: ResolvedTarget, + optimize: OptimizeMode, + roc_modules: modules.RocModules, + strip: bool, + omit_frame_pointer: ?bool, +) *Step.Compile { + const lib = b.addLibrary(.{ + .name = name, + .linkage = .static, + .root_module = b.createModule(.{ + .root_source_file = b.path(host_path), + .target = target, + .optimize = optimize, + .strip = strip, + .omit_frame_pointer = omit_frame_pointer, + .pic = true, // Enable Position Independent Code for PIE compatibility + }), + }); + configureBackend(lib, target); + lib.root_module.addImport("builtins", roc_modules.builtins); + lib.root_module.addImport("build_options", roc_modules.build_options); + // Don't bundle compiler-rt in host libraries - roc_shim provides it + // Bundling it here causes duplicate symbol errors on Windows + lib.bundle_compiler_rt = false; + + return lib; +} + +/// Builds a test platform host library and sets up a step to copy it to the target-specific directory. +/// Returns the final step for dependency wiring. +fn buildAndCopyTestPlatformHostLib( + b: *std.Build, + platform_dir: []const u8, + target: ResolvedTarget, + target_name: []const u8, + optimize: OptimizeMode, + roc_modules: modules.RocModules, + strip: bool, + omit_frame_pointer: ?bool, +) *Step { + const lib = createTestPlatformHostLib( + b, + b.fmt("test_platform_{s}_host_{s}", .{ platform_dir, target_name }), + b.pathJoin(&.{ "test", platform_dir, "platform/host.zig" }), + target, + optimize, + roc_modules, + strip, + omit_frame_pointer, + ); + + // Use correct filename for target platform + const host_filename = if (target.result.os.tag == .windows) "host.lib" else "libhost.a"; + const archive_path = b.pathJoin(&.{ "test", platform_dir, "platform/targets", target_name, host_filename }); + + const copy_step = b.addUpdateSourceFiles(); + copy_step.addCopyFileToSource(lib.getEmittedBin(), archive_path); + + // Workaround for Zig bug https://codeberg.org/ziglang/zig/issues/30572 + // Zig's archive generator doesn't add the required padding byte after odd-sized + // members, causing lld to reject the archive with: + // "Archive::children failed: truncated or malformed archive" + if (target.result.os.tag != .windows) { + const fix_step = FixArchivePaddingStep.create(b, archive_path); + fix_step.step.dependOn(©_step.step); + return &fix_step.step; + } + + return ©_step.step; +} + +// Workaround for Zig bug https://codeberg.org/ziglang/zig/issues/30572 +const FixArchivePaddingStep = struct { + step: Step, + archive_path: []const u8, + + fn create(b: *std.Build, archive_path: []const u8) *FixArchivePaddingStep { + const self = b.allocator.create(FixArchivePaddingStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.{ + .id = Step.Id.custom, + .name = "fix-archive-padding", + .owner = b, + .makeFn = make, + }), + .archive_path = archive_path, + }; + return self; + } + + fn make(step: *Step, options: Step.MakeOptions) !void { + _ = options; + const self: *FixArchivePaddingStep = @fieldParentPtr("step", step); + + const file = std.fs.cwd().openFile(self.archive_path, .{ .mode = .read_write }) catch |err| { + std.debug.print("Warning: Could not open archive {s}: {s}\n", .{ self.archive_path, @errorName(err) }); + return; + }; + defer file.close(); + + const stat = try file.stat(); + const file_size = stat.size; + + // AR format requires archives to end on an even byte boundary. + // If file size is odd, append a newline padding byte. + if (file_size % 2 == 1) { + try file.seekTo(file_size); + try file.writeAll("\n"); + } + } +}; + +/// Custom build step that clears the Roc cache directory. +/// Uses Zig's native filesystem APIs for cross-platform support. +const ClearRocCacheStep = struct { + step: Step, + + fn create(b: *std.Build) *ClearRocCacheStep { + const self = b.allocator.create(ClearRocCacheStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.{ + .id = Step.Id.custom, + .name = "clear-roc-cache", + .owner = b, + .makeFn = make, + }), + }; + return self; + } + + fn make(step: *Step, options: Step.MakeOptions) !void { + _ = options; + + const allocator = step.owner.allocator; + + // Get the cache directory path using the same logic as cache_config.zig + const cache_dir = getCacheDir(allocator) catch |err| { + std.debug.print("Warning: Could not determine cache directory: {s}\n", .{@errorName(err)}); + return; + }; + defer allocator.free(cache_dir); + + // Check if cache directory exists before trying to delete + std.fs.cwd().access(cache_dir, .{}) catch { + // Cache doesn't exist, nothing to do + std.debug.print("Roc cache not found (nothing to clear)\n", .{}); + return; + }; + + // Try to delete the cache directory + std.fs.cwd().deleteTree(cache_dir) catch |err| { + std.debug.print("Warning: Could not clear cache at {s}: {s}\n", .{ cache_dir, @errorName(err) }); + return; + }; + + std.debug.print("Cleared roc cache at {s}\n", .{cache_dir}); + } + + /// Get the Roc cache directory path (matches cache_config.zig logic) + fn getCacheDir(allocator: std.mem.Allocator) ![]u8 { + const cache_dir_name = switch (builtin.os.tag) { + .windows => "Roc", + else => "roc", + }; + + // Respect XDG_CACHE_HOME if set + if (std.process.getEnvVarOwned(allocator, "XDG_CACHE_HOME")) |xdg_cache| { + defer allocator.free(xdg_cache); + return std.fs.path.join(allocator, &[_][]const u8{ xdg_cache, cache_dir_name }); + } else |_| { + // Fall back to platform defaults + const home_env = switch (builtin.os.tag) { + .windows => "APPDATA", + else => "HOME", + }; + + const home_dir = std.process.getEnvVarOwned(allocator, home_env) catch { + return error.NoHomeDirectory; + }; + defer allocator.free(home_dir); + + return switch (builtin.os.tag) { + .linux => std.fs.path.join(allocator, &[_][]const u8{ home_dir, ".cache", cache_dir_name }), + .macos => std.fs.path.join(allocator, &[_][]const u8{ home_dir, "Library", "Caches", cache_dir_name }), + .windows => std.fs.path.join(allocator, &[_][]const u8{ home_dir, cache_dir_name }), + else => std.fs.path.join(allocator, &[_][]const u8{ home_dir, ".cache", cache_dir_name }), + }; + } + } +}; + +const PrintBuildSuccessStep = struct { + step: Step, + + fn create(b: *std.Build) *PrintBuildSuccessStep { + const self = b.allocator.create(PrintBuildSuccessStep) catch @panic("OOM"); + self.* = .{ + .step = Step.init(.{ + .id = Step.Id.custom, + .name = "print-build-success", + .owner = b, + .makeFn = make, + }), + }; + return self; + } + + fn make(step: *Step, options: Step.MakeOptions) !void { + _ = step; + _ = options; + std.debug.print("Build succeeded!\n", .{}); + } +}; + +/// Create a step that clears the Roc cache directory. +/// This is useful when rebuilding test platforms to ensure stale cached hosts aren't used. +fn createClearCacheStep(b: *std.Build) *Step { + const clear_cache = ClearRocCacheStep.create(b); + return &clear_cache.step; +} + +fn setupTestPlatforms( + b: *std.Build, + target: ResolvedTarget, + optimize: OptimizeMode, + roc_modules: modules.RocModules, + test_platforms_step: *Step, + strip: bool, + omit_frame_pointer: ?bool, +) void { + // Clear the Roc cache when test platforms are rebuilt to ensure stale cached hosts aren't used + const clear_cache_step = createClearCacheStep(b); + const native_target_name = roc_target.RocTarget.fromStdTarget(target.result).toName(); + + // Build all test platforms for native target + for (all_test_platform_dirs) |platform_dir| { + const copy_step = buildAndCopyTestPlatformHostLib( + b, + platform_dir, + target, + native_target_name, + optimize, + roc_modules, + strip, + omit_frame_pointer, + ); + clear_cache_step.dependOn(copy_step); + } + + // Cross-compile for musl targets (glibc not needed for test-platforms step) + for (musl_cross_targets) |cross_target| { + const cross_resolved_target = b.resolveTargetQuery(cross_target.query); + + for (all_test_platform_dirs) |platform_dir| { + const copy_step = buildAndCopyTestPlatformHostLib( + b, + platform_dir, + cross_resolved_target, + cross_target.name, + optimize, + roc_modules, + strip, + omit_frame_pointer, + ); + clear_cache_step.dependOn(copy_step); + } + } + + // Cross-compile for Windows targets + for (windows_cross_targets) |cross_target| { + const cross_resolved_target = b.resolveTargetQuery(cross_target.query); + + for (all_test_platform_dirs) |platform_dir| { + const copy_step = buildAndCopyTestPlatformHostLib( + b, + platform_dir, + cross_resolved_target, + cross_target.name, + optimize, + roc_modules, + strip, + omit_frame_pointer, + ); + clear_cache_step.dependOn(copy_step); + } + } + + // Build the wasm test platform host for wasm32-freestanding + { + const wasm_target = b.resolveTargetQuery(.{ .cpu_arch = .wasm32, .os_tag = .freestanding, .abi = .none }); + const copy_step = buildAndCopyTestPlatformHostLib( + b, + "wasm", + wasm_target, + "wasm32", + optimize, + roc_modules, + strip, + omit_frame_pointer, + ); + clear_cache_step.dependOn(copy_step); + } + + b.getInstallStep().dependOn(clear_cache_step); + test_platforms_step.dependOn(clear_cache_step); +} + pub fn build(b: *std.Build) void { // build steps const run_step = b.step("run", "Build and run the roc cli"); const roc_step = b.step("roc", "Build the roc compiler without running it"); const test_step = b.step("test", "Run all tests included in src/tests.zig"); + const minici_step = b.step("minici", "Run a subset of CI build and test steps"); + const checkfx_step = b.step("checkfx", "Check that every .roc file in test/fx has a corresponding test"); const fmt_step = b.step("fmt", "Format all zig code"); const check_fmt_step = b.step("check-fmt", "Check formatting of all zig code"); const snapshot_step = b.step("snapshot", "Run the snapshot tool to update snapshot files"); const playground_step = b.step("playground", "Build the WASM playground"); - const playground_test_step = b.step("playground-test", "Build the integration test suite for the WASM playground"); + const playground_test_step = b.step("test-playground", "Build the integration test suite for the WASM playground"); + const serialization_size_step = b.step("test-serialization-sizes", "Verify Serialized types have platform-independent sizes"); + const wasm_static_lib_test_step = b.step("test-wasm-static-lib", "Test WASM static library builds with bytebox"); + const test_cli_step = b.step("test-cli", "Test the roc CLI by running test programs"); + const test_platforms_step = b.step("test-platforms", "Build test platform host libraries"); // general configuration - const target = b.standardTargetOptions(.{ .default_target = .{ - .abi = if (builtin.target.os.tag == .linux) .musl else null, - } }); + const target = blk: { + var default_target_query: std.Target.Query = .{ + .abi = if (builtin.target.os.tag == .linux) .musl else null, + }; + + // Use baseline x86_64 CPU for Valgrind compatibility on CI (Valgrind 3.18.1 doesn't support AVX-512) + const is_ci = std.process.getEnvVarOwned(b.allocator, "CI") catch null; + if (is_ci != null and builtin.target.cpu.arch == .x86_64 and builtin.target.os.tag == .linux) { + default_target_query.cpu_model = .{ .explicit = &std.Target.x86.cpu.x86_64 }; + } + + break :blk b.standardTargetOptions(.{ .default_target = default_target_query }); + }; const optimize = b.standardOptimizeOption(.{}); - const strip = b.option(bool, "strip", "Omit debug information"); + const strip_flag = b.option(bool, "strip", "Omit debug information"); const no_bin = b.option(bool, "no-bin", "Skip emitting binaries (important for fast incremental compilation)") orelse false; const trace_eval = b.option(bool, "trace-eval", "Enable detailed evaluation tracing for debugging") orelse (optimize == .Debug); + const trace_refcount = b.option(bool, "trace-refcount", "Enable detailed refcount tracing for debugging memory issues") orelse false; + const trace_modules = b.option(bool, "trace-modules", "Enable module compilation and import resolution tracing") orelse false; + + const parsed_args = parseBuildArgs(b); + const run_args = parsed_args.run_args; + const test_filters = parsed_args.test_filters; // llvm configuration const use_system_llvm = b.option(bool, "system-llvm", "Attempt to automatically detect and use system installed llvm") orelse false; - const enable_llvm = b.option(bool, "llvm", "Build roc with the llvm backend") orelse use_system_llvm; + const enable_llvm = !use_system_llvm; // removed build flag `-Dllvm`, we include LLVM libraries by default now const user_llvm_path = b.option([]const u8, "llvm-path", "Path to llvm. This path must contain the bin, lib, and include directory."); // Since zig afl is broken currently, default to system afl. const use_system_afl = b.option(bool, "system-afl", "Attempt to automatically detect and use system installed afl++") orelse true; @@ -44,7 +1716,7 @@ pub fn build(b: *std.Build) void { // tracy profiler configuration const flag_enable_tracy = b.option([]const u8, "tracy", "Enable Tracy integration. Supply path to Tracy source"); - const flag_tracy_callstack = b.option(bool, "tracy-callstack", "Include callstack information with Tracy data. Does nothing if -Dtracy is not provided") orelse (flag_enable_tracy != null); + const flag_tracy_callstack = b.option(bool, "tracy-callstack", "Include callstack information with Tracy data. Does nothing if -Dtracy is not provided") orelse false; const flag_tracy_allocation = b.option(bool, "tracy-allocation", "Include allocation information with Tracy data. Does nothing if -Dtracy is not provided") orelse (flag_enable_tracy != null); const flag_tracy_callstack_depth: u32 = b.option(u32, "tracy-callstack-depth", "Declare callstack depth for Tracy data. Does nothing if -Dtracy_callstack is not provided") orelse 10; if (flag_tracy_callstack) { @@ -55,16 +1727,46 @@ pub fn build(b: *std.Build) void { const build_options = b.addOptions(); build_options.addOption(bool, "enable_tracy", flag_enable_tracy != null); build_options.addOption(bool, "trace_eval", trace_eval); + build_options.addOption(bool, "trace_refcount", trace_refcount); + build_options.addOption(bool, "trace_modules", trace_modules); build_options.addOption([]const u8, "compiler_version", getCompilerVersion(b, optimize)); - if (target.result.os.tag == .macos and flag_tracy_callstack) { - std.log.warn("Tracy callstack does not work on MacOS, disabling.", .{}); - build_options.addOption(bool, "enable_tracy_callstack", false); - } else { - build_options.addOption(bool, "enable_tracy_callstack", flag_tracy_callstack); - } + build_options.addOption(bool, "enable_tracy_callstack", flag_tracy_callstack); build_options.addOption(bool, "enable_tracy_allocation", flag_tracy_allocation); build_options.addOption(u32, "tracy_callstack_depth", flag_tracy_callstack_depth); - build_options.addOption(bool, "target_is_native", target.query.isNative()); + + // Calculate effective strip value + // - If strip is explicitly set by user, use that (warn if tracy_callstack is also set) + // - Otherwise, default to stripping if not debug, unless tracy_callstack is enabled + const strip: bool = blk: { + if (strip_flag) |strip_bool| { + // User explicitly set strip + if (strip_bool and flag_tracy_callstack) { + std.log.warn("Both -Dstrip and -Dtracy-callstack are enabled. " ++ + "Stripping will remove callstack information needed by Tracy.", .{}); + } + break :blk strip_bool; + } else { + // User did not set strip - use defaults + if (flag_tracy_callstack) { + // Don't strip when tracy callstack is enabled (preserves debug info) + break :blk false; + } else { + // Default: strip in release modes + break :blk optimize != .Debug; + } + } + }; + + // Don't omit frame pointer when tracy callstack is enabled (needed for callstack capture) + const omit_frame_pointer: ?bool = if (flag_tracy_callstack) false else null; + + const target_is_native = + // `query.isNative()` becomes false as soon as users override CPU features (e.g. -Dcpu=x86_64_v3), + // but we still want to treat those builds as native so macOS can link against real FSEvents. + target.result.os.tag == builtin.target.os.tag and + target.result.cpu.arch == builtin.target.cpu.arch and + target.result.abi == builtin.target.abi; + build_options.addOption(bool, "target_is_native", target_is_native); // We use zstd for `roc bundle` and `roc unbundle` and downloading .tar.zst bundles. const zstd = b.dependency("zstd", .{ @@ -74,35 +1776,219 @@ pub fn build(b: *std.Build) void { const roc_modules = modules.RocModules.create(b, build_options, zstd); - // add main roc exe - const roc_exe = addMainExe(b, roc_modules, target, optimize, strip, enable_llvm, use_system_llvm, user_llvm_path, flag_enable_tracy, zstd) orelse return; + // Build-time compiler for builtin .roc modules with caching + // + // Changes to .roc files in src/build/roc/ are automatically detected and trigger recompilation. + // However, if you modify the compiler itself (e.g., parse, can, check modules) and want those + // changes reflected in the builtin .bin files, you need to run: zig build rebuild-builtins + // + // We cache the builtin compiler executable to avoid ~doubling normal build times. + // CI always rebuilds from scratch, so it's not affected by this caching. + const builtin_roc_path = "src/build/roc/Builtin.roc"; + + // Check if we need to rebuild builtins by comparing .roc and .bin file timestamps + const should_rebuild_builtins = blk: { + const builtin_bin_path = "zig-out/builtins/Builtin.bin"; + + const roc_stat = std.fs.cwd().statFile(builtin_roc_path) catch break :blk true; + const bin_stat = std.fs.cwd().statFile(builtin_bin_path) catch break :blk true; + + // If .roc file is newer than .bin file, rebuild + if (roc_stat.mtime > bin_stat.mtime) { + break :blk true; + } + + // Check if builtin_indices.bin exists + _ = std.fs.cwd().statFile("zig-out/builtins/builtin_indices.bin") catch break :blk true; + + // Builtin.bin exists and is up-to-date + break :blk false; + }; + + const write_compiled_builtins = b.addWriteFiles(); + + // Regenerate .bin files if necessary + if (should_rebuild_builtins) { + const run_builtin_compiler = createAndRunBuiltinCompiler(b, roc_modules, flag_enable_tracy, &.{builtin_roc_path}); + write_compiled_builtins.step.dependOn(&run_builtin_compiler.step); + } + + // Copy Builtin.bin from zig-out/builtins/ + _ = write_compiled_builtins.addCopyFile( + .{ .cwd_relative = "zig-out/builtins/Builtin.bin" }, + "Builtin.bin", + ); + + // Copy the source Builtin.roc file for embedding + _ = write_compiled_builtins.addCopyFile( + b.path(builtin_roc_path), + "Builtin.roc", + ); + + // Copy builtin_indices.bin + _ = write_compiled_builtins.addCopyFile( + .{ .cwd_relative = "zig-out/builtins/builtin_indices.bin" }, + "builtin_indices.bin", + ); + + // Generate compiled_builtins.zig with hardcoded Builtin module + const builtins_source_str = + \\pub const builtin_bin = @embedFile("Builtin.bin"); + \\pub const builtin_source = @embedFile("Builtin.roc"); + \\pub const builtin_indices_bin = @embedFile("builtin_indices.bin"); + \\ + ; + + const compiled_builtins_source = write_compiled_builtins.add( + "compiled_builtins.zig", + builtins_source_str, + ); + + const compiled_builtins_module = b.createModule(.{ + .root_source_file = compiled_builtins_source, + }); + + roc_modules.repl.addImport("compiled_builtins", compiled_builtins_module); + roc_modules.compile.addImport("compiled_builtins", compiled_builtins_module); + roc_modules.eval.addImport("compiled_builtins", compiled_builtins_module); + + // Setup test platform host libraries + setupTestPlatforms(b, target, optimize, roc_modules, test_platforms_step, strip, omit_frame_pointer); + + const roc_exe = addMainExe(b, roc_modules, target, optimize, strip, omit_frame_pointer, enable_llvm, use_system_llvm, user_llvm_path, flag_enable_tracy, zstd, compiled_builtins_module, write_compiled_builtins, flag_enable_tracy) orelse return; roc_modules.addAll(roc_exe); - install_and_run(b, no_bin, roc_exe, roc_step, run_step); + install_and_run(b, no_bin, roc_exe, roc_step, run_step, run_args); + + // Clear the Roc cache when building the compiler to ensure stale cached artifacts aren't used + const clear_cache_step = createClearCacheStep(b); + roc_step.dependOn(clear_cache_step); + b.getInstallStep().dependOn(clear_cache_step); + + // Unified test platform runner (replaces fx_cross_runner and int_cross_runner) + const test_runner_exe = b.addExecutable(.{ + .name = "test_runner", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/cli/test/test_runner.zig"), + .target = target, + .optimize = optimize, + }), + }); + b.installArtifact(test_runner_exe); + + // CLI integration tests - run actual roc programs like CI does + // These tests can run in parallel since each build uses content-hashed shim files + if (!no_bin) { + const install = b.addInstallArtifact(roc_exe, .{}); + const install_runner = b.addInstallArtifact(test_runner_exe, .{}); + + // Test int platform (native mode only for now) + const run_int_tests = b.addRunArtifact(test_runner_exe); + run_int_tests.addArg("zig-out/bin/roc"); + run_int_tests.addArg("int"); + run_int_tests.addArg("--mode=native"); + run_int_tests.step.dependOn(&install.step); + run_int_tests.step.dependOn(&install_runner.step); + run_int_tests.step.dependOn(test_platforms_step); + test_cli_step.dependOn(&run_int_tests.step); + + // Test str platform (native mode only for now) + const run_str_tests = b.addRunArtifact(test_runner_exe); + run_str_tests.addArg("zig-out/bin/roc"); + run_str_tests.addArg("str"); + run_str_tests.addArg("--mode=native"); + run_str_tests.step.dependOn(&install.step); + run_str_tests.step.dependOn(&install_runner.step); + run_str_tests.step.dependOn(test_platforms_step); + test_cli_step.dependOn(&run_str_tests.step); + + // Roc subcommands integration test + const roc_subcommands_test = b.addTest(.{ + .name = "roc_subcommands_test", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/cli/test/roc_subcommands.zig"), + .target = target, + .optimize = optimize, + }), + .filters = test_filters, + }); + + const run_roc_subcommands_test = b.addRunArtifact(roc_subcommands_test); + if (run_args.len != 0) { + run_roc_subcommands_test.addArgs(run_args); + } + run_roc_subcommands_test.step.dependOn(&install.step); + run_roc_subcommands_test.step.dependOn(test_platforms_step); + test_cli_step.dependOn(&run_roc_subcommands_test.step); + } + + // Manual rebuild command: zig build rebuild-builtins + // Use this after making compiler changes to ensure those changes are reflected in builtins + const rebuild_builtins_step = b.step( + "rebuild-builtins", + "Force rebuild of all builtin modules (*.roc -> *.bin)", + ); + + // Clean zig-out/ to ensure a fresh rebuild of builtins + // Note: We don't delete .zig-cache because it contains build options needed during compilation. + const clean_out_step = b.addRemoveDirTree(b.path("zig-out")); + + // Also clear the roc cache to avoid stale cached modules with old struct layouts + const clear_roc_cache_step = createClearCacheStep(b); + + // Discover .roc files again for the rebuild command + const roc_files_force = discoverBuiltinRocFiles(b) catch |err| { + std.debug.print("Failed to discover .roc files for rebuild: {}\n", .{err}); + return; + }; + + const run_builtin_compiler_force = createAndRunBuiltinCompiler(b, roc_modules, flag_enable_tracy, roc_files_force); + run_builtin_compiler_force.step.dependOn(&clean_out_step.step); + run_builtin_compiler_force.step.dependOn(clear_roc_cache_step); + rebuild_builtins_step.dependOn(&run_builtin_compiler_force.step); + + // Add the compiled builtins module to roc exe and make it depend on the builtins being ready + roc_exe.root_module.addImport("compiled_builtins", compiled_builtins_module); + roc_exe.step.dependOn(&write_compiled_builtins.step); // Add snapshot tool const snapshot_exe = b.addExecutable(.{ .name = "snapshot", - .root_source_file = b.path("src/snapshot_tool/main.zig"), - .target = target, - .optimize = optimize, - .link_libc = true, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/snapshot_tool/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }), }); + configureBackend(snapshot_exe, target); roc_modules.addAll(snapshot_exe); + snapshot_exe.root_module.addImport("compiled_builtins", compiled_builtins_module); + snapshot_exe.step.dependOn(&write_compiled_builtins.step); add_tracy(b, roc_modules.build_options, snapshot_exe, target, false, flag_enable_tracy); - install_and_run(b, no_bin, snapshot_exe, snapshot_step, snapshot_step); + install_and_run(b, no_bin, snapshot_exe, snapshot_step, snapshot_step, run_args); const playground_exe = b.addExecutable(.{ .name = "playground", - .root_source_file = b.path("src/playground_wasm/main.zig"), - .target = b.resolveTargetQuery(.{ - .cpu_arch = .wasm32, - .os_tag = .freestanding, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/playground_wasm/main.zig"), + .target = b.resolveTargetQuery(.{ + .cpu_arch = .wasm32, + .os_tag = .freestanding, + }), + .optimize = optimize, }), - .optimize = optimize, }); + configureBackend(playground_exe, b.resolveTargetQuery(.{ + .cpu_arch = .wasm32, + .os_tag = .freestanding, + })); playground_exe.entry = .disabled; playground_exe.rdynamic = true; + playground_exe.link_function_sections = true; + playground_exe.import_memory = false; roc_modules.addAll(playground_exe); + playground_exe.root_module.addImport("compiled_builtins", compiled_builtins_module); + playground_exe.step.dependOn(&write_compiled_builtins.step); add_tracy(b, roc_modules.build_options, playground_exe, b.resolveTargetQuery(.{ .cpu_arch = .wasm32, @@ -121,10 +2007,13 @@ pub fn build(b: *std.Build) void { const playground_test_install = blk: { const playground_integration_test_exe = b.addExecutable(.{ .name = "playground_integration_test", - .root_source_file = b.path("test/playground-integration/main.zig"), - .target = target, - .optimize = optimize, + .root_module = b.createModule(.{ + .root_source_file = b.path("test/playground-integration/main.zig"), + .target = target, + .optimize = optimize, + }), }); + configureBackend(playground_integration_test_exe, target); playground_integration_test_exe.root_module.addImport("bytebox", bytebox.module("bytebox")); playground_integration_test_exe.root_module.addImport("build_options", build_options.createModule()); roc_modules.addAll(playground_integration_test_exe); @@ -135,8 +2024,8 @@ pub fn build(b: *std.Build) void { playground_test_step.dependOn(&install.step); const run_playground_test = b.addRunArtifact(playground_integration_test_exe); - if (b.args) |args| { - run_playground_test.addArgs(args); + if (run_args.len != 0) { + run_playground_test.addArgs(run_args); } run_playground_test.step.dependOn(&install.step); playground_test_step.dependOn(&run_playground_test.step); @@ -144,63 +2033,218 @@ pub fn build(b: *std.Build) void { break :blk install; }; + // Add serialization size check + // This verifies that Serialized types have the same size on 32-bit and 64-bit platforms + // using compile-time assertions + { + // Build for native - will fail at compile time if sizes don't match expected + const size_check_native = b.addExecutable(.{ + .name = "serialization_size_check_native", + .root_module = b.createModule(.{ + .root_source_file = b.path("test/serialization_size_check.zig"), + .target = target, + .optimize = .Debug, + }), + }); + configureBackend(size_check_native, target); + roc_modules.addAll(size_check_native); + + // Build for wasm32 (32-bit) - will fail at compile time if sizes don't match expected + const size_check_wasm32 = b.addExecutable(.{ + .name = "serialization_size_check_wasm32", + .root_module = b.createModule(.{ + .root_source_file = b.path("test/serialization_size_check.zig"), + .target = b.resolveTargetQuery(.{ + .cpu_arch = .wasm32, + .os_tag = .freestanding, + }), + .optimize = .Debug, + }), + }); + configureBackend(size_check_wasm32, b.resolveTargetQuery(.{ + .cpu_arch = .wasm32, + .os_tag = .freestanding, + })); + size_check_wasm32.entry = .disabled; + size_check_wasm32.rdynamic = true; + roc_modules.addAll(size_check_wasm32); + + // Run the native version to confirm (wasm32 build is enough to verify 32-bit) + const run_native = b.addRunArtifact(size_check_native); + + // The test passes if both executables build successfully (compile-time checks pass) + // and the native one runs without error + serialization_size_step.dependOn(&size_check_native.step); + serialization_size_step.dependOn(&size_check_wasm32.step); + serialization_size_step.dependOn(&run_native.step); + } + + // Build WASM static library test runner with bytebox + // This test requires the WASM file to be built separately via `roc build test/wasm/app.roc --target=wasm32` + { + const wasm_test_exe = b.addExecutable(.{ + .name = "wasm_static_lib_test", + .root_module = b.createModule(.{ + .root_source_file = b.path("test/wasm/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + configureBackend(wasm_test_exe, target); + wasm_test_exe.root_module.addImport("bytebox", bytebox.module("bytebox")); + + const install = b.addInstallArtifact(wasm_test_exe, .{}); + wasm_static_lib_test_step.dependOn(&install.step); + + const run_wasm_test = b.addRunArtifact(wasm_test_exe); + if (run_args.len != 0) { + run_wasm_test.addArgs(run_args); + } + run_wasm_test.step.dependOn(&install.step); + wasm_static_lib_test_step.dependOn(&run_wasm_test.step); + } + + // Check fx platform test coverage convenience step + const checkfx_inner = CheckFxStep.create(b); + checkfx_step.dependOn(&checkfx_inner.step); + + // Mini CI convenience step: runs a sequence of common build and test commands in order. + const minici_inner = MiniCiStep.create(b); + minici_step.dependOn(&minici_inner.step); + // Create and add module tests - const module_tests = roc_modules.createModuleTests(b, target, optimize, zstd); - for (module_tests) |module_test| { + const module_tests_result = roc_modules.createModuleTests(b, target, optimize, zstd, test_filters); + const tests_summary = TestsSummaryStep.create(b, test_filters, module_tests_result.forced_passes); + for (module_tests_result.tests) |module_test| { + // Add compiled builtins to check, repl, and eval module tests + if (std.mem.eql(u8, module_test.test_step.name, "check") or std.mem.eql(u8, module_test.test_step.name, "repl") or std.mem.eql(u8, module_test.test_step.name, "eval")) { + module_test.test_step.root_module.addImport("compiled_builtins", compiled_builtins_module); + module_test.test_step.step.dependOn(&write_compiled_builtins.step); + } + + if (run_args.len != 0) { + module_test.run_step.addArgs(run_args); + } + + // Create individual test step for this module + const test_exe_name = module_test.test_step.name; + const step_name = b.fmt("test-{s}", .{test_exe_name}); + const individual_test_step = b.step(step_name, b.fmt("Run {s} tests only", .{test_exe_name})); + + // Create run step that accepts command line args (including --test-filter) + const individual_run = b.addRunArtifact(module_test.test_step); + if (run_args.len != 0) { + individual_run.addArgs(run_args); + } + individual_test_step.dependOn(&individual_run.step); + b.default_step.dependOn(&module_test.test_step.step); - test_step.dependOn(&module_test.run_step.step); + tests_summary.addRun(&module_test.run_step.step); } // Add snapshot tool test - const snapshot_test = b.addTest(.{ - .name = "snapshot_tool_test", - .root_source_file = b.path("src/snapshot_tool/main.zig"), - .target = target, - .optimize = optimize, - .link_libc = true, - }); - roc_modules.addAll(snapshot_test); - add_tracy(b, roc_modules.build_options, snapshot_test, target, false, flag_enable_tracy); + const enable_snapshot_tests = b.option(bool, "snapshot-tests", "Enable snapshot tests") orelse true; + if (enable_snapshot_tests) { + const snapshot_test = b.addTest(.{ + .name = "snapshot_tool_test", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/snapshot_tool/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }), + .filters = test_filters, + }); + roc_modules.addAll(snapshot_test); + snapshot_test.root_module.addImport("compiled_builtins", compiled_builtins_module); + snapshot_test.step.dependOn(&write_compiled_builtins.step); + add_tracy(b, roc_modules.build_options, snapshot_test, target, false, flag_enable_tracy); - const run_snapshot_test = b.addRunArtifact(snapshot_test); - test_step.dependOn(&run_snapshot_test.step); - - // Add CLI test - const cli_test = b.addTest(.{ - .name = "cli_test", - .root_source_file = b.path("src/cli/main.zig"), - .target = target, - .optimize = optimize, - .link_libc = true, - }); - roc_modules.addAll(cli_test); - cli_test.linkLibrary(zstd.artifact("zstd")); - add_tracy(b, roc_modules.build_options, cli_test, target, false, flag_enable_tracy); - - const run_cli_test = b.addRunArtifact(cli_test); - test_step.dependOn(&run_cli_test.step); - - // Add watch tests - const watch_test = b.addTest(.{ - .name = "watch_test", - .root_source_file = b.path("src/watch/watch.zig"), - .target = target, - .optimize = optimize, - .link_libc = true, - }); - roc_modules.addAll(watch_test); - add_tracy(b, roc_modules.build_options, watch_test, target, false, flag_enable_tracy); - - // Link platform-specific libraries for file watching - if (target.result.os.tag == .macos) { - watch_test.linkFramework("CoreFoundation"); - watch_test.linkFramework("CoreServices"); - } else if (target.result.os.tag == .windows) { - watch_test.linkSystemLibrary("kernel32"); + const run_snapshot_test = b.addRunArtifact(snapshot_test); + if (run_args.len != 0) { + run_snapshot_test.addArgs(run_args); + } + tests_summary.addRun(&run_snapshot_test.step); } - const run_watch_test = b.addRunArtifact(watch_test); - test_step.dependOn(&run_watch_test.step); + // Add CLI test + const enable_cli_tests = b.option(bool, "cli-tests", "Enable cli tests") orelse true; + if (enable_cli_tests) { + const cli_test = b.addTest(.{ + .name = "cli_test", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/cli/main.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }), + .filters = test_filters, + }); + roc_modules.addAll(cli_test); + cli_test.linkLibrary(zstd.artifact("zstd")); + add_tracy(b, roc_modules.build_options, cli_test, target, false, flag_enable_tracy); + cli_test.root_module.addImport("compiled_builtins", compiled_builtins_module); + cli_test.step.dependOn(&write_compiled_builtins.step); + + const run_cli_test = b.addRunArtifact(cli_test); + if (run_args.len != 0) { + run_cli_test.addArgs(run_args); + } + tests_summary.addRun(&run_cli_test.step); + } + + // Add watch tests + const enable_watch_tests = b.option(bool, "watch-tests", "Enable watch tests") orelse true; + if (enable_watch_tests) { + const watch_test = b.addTest(.{ + .name = "watch_test", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/watch/watch.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }), + .filters = test_filters, + }); + roc_modules.addAll(watch_test); + add_tracy(b, roc_modules.build_options, watch_test, target, false, flag_enable_tracy); + + // Link platform-specific libraries for file watching + if (target.result.os.tag == .macos and target_is_native) { + watch_test.linkFramework("CoreFoundation"); + watch_test.linkFramework("CoreServices"); + } else if (target.result.os.tag == .windows) { + watch_test.linkSystemLibrary("kernel32"); + } + + const run_watch_test = b.addRunArtifact(watch_test); + if (run_args.len != 0) { + run_watch_test.addArgs(run_args); + } + tests_summary.addRun(&run_watch_test.step); + } + + // Add check for forbidden patterns in type checker code + const check_patterns = CheckTypeCheckerPatternsStep.create(b); + test_step.dependOn(&check_patterns.step); + + // Add check for @enumFromInt(0) usage + const check_enum_from_int = CheckEnumFromIntZeroStep.create(b); + test_step.dependOn(&check_enum_from_int.step); + + // Add check for unused variable suppression patterns + const check_unused = CheckUnusedSuppressionStep.create(b); + test_step.dependOn(&check_unused.step); + + // Check for @panic and std.debug.panic in interpreter and builtins + const check_panic = CheckPanicStep.create(b); + test_step.dependOn(&check_panic.step); + + // Add check for global stdio usage in CLI code + const check_cli_stdio = CheckCliGlobalStdioStep.create(b); + test_step.dependOn(&check_cli_stdio.step); + + test_step.dependOn(&tests_summary.step); b.default_step.dependOn(playground_step); { @@ -217,14 +2261,80 @@ pub fn build(b: *std.Build) void { check_fmt_step.dependOn(&check_fmt.step); const fuzz = b.option(bool, "fuzz", "Build fuzz targets including AFL++ and tooling") orelse false; - const is_native = target.query.isNativeCpu() and target.query.isNativeOs() and (target.query.isNativeAbi() or target.result.abi.isMusl()); const is_windows = target.result.os.tag == .windows; + // fx platform effectful functions test - only run when not cross-compiling + if (isNativeishOrMusl(target)) { + // Determine the appropriate target for the fx platform host library. + // On Linux, we need to use musl explicitly because the CLI's findHostLibrary + // looks for targets/x64musl/libhost.a first, and musl produces proper static binaries. + const native_fx_target_dir = roc_target.RocTarget.fromStdTarget(target.result).toName(); + const fx_host_target, const fx_host_target_dir: ?[]const u8 = switch (target.result.os.tag) { + .linux => switch (target.result.cpu.arch) { + .x86_64 => .{ b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl }), "x64musl" }, + .aarch64 => .{ b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl }), "arm64musl" }, + else => .{ target, native_fx_target_dir }, + }, + .windows => switch (target.result.cpu.arch) { + .x86_64 => .{ target, "x64win" }, + .aarch64 => .{ target, "arm64win" }, + else => .{ target, native_fx_target_dir }, + }, + else => .{ target, native_fx_target_dir }, + }; + + // Create fx test platform host static library + const test_platform_fx_host_lib = createTestPlatformHostLib( + b, + "test_platform_fx_host", + "test/fx/platform/host.zig", + fx_host_target, + optimize, + roc_modules, + strip, + omit_frame_pointer, + ); + + // Copy the fx test platform host library to the source directory + const copy_test_fx_host = b.addUpdateSourceFiles(); + const test_fx_host_filename = if (target.result.os.tag == .windows) "host.lib" else "libhost.a"; + copy_test_fx_host.addCopyFileToSource(test_platform_fx_host_lib.getEmittedBin(), b.pathJoin(&.{ "test/fx/platform", test_fx_host_filename })); + b.getInstallStep().dependOn(©_test_fx_host.step); + + // Also copy to the target-specific directory so findHostLibrary finds it + if (fx_host_target_dir) |target_dir| { + copy_test_fx_host.addCopyFileToSource( + test_platform_fx_host_lib.getEmittedBin(), + b.pathJoin(&.{ "test/fx/platform/targets", target_dir, test_fx_host_filename }), + ); + } + + const fx_platform_test = b.addTest(.{ + .name = "fx_platform_test", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/cli/test/fx_platform_test.zig"), + .target = target, + .optimize = optimize, + }), + .filters = test_filters, + }); + + const run_fx_platform_test = b.addRunArtifact(fx_platform_test); + if (run_args.len != 0) { + run_fx_platform_test.addArgs(run_args); + } + // Ensure host library is copied before running the test + run_fx_platform_test.step.dependOn(©_test_fx_host.step); + // Ensure roc binary is built before running the test (tests invoke roc CLI) + run_fx_platform_test.step.dependOn(roc_step); + tests_summary.addRun(&run_fx_platform_test.step); + } + var build_afl = false; - if (!is_native) { + if (!isNativeishOrMusl(target)) { std.log.warn("Cross compilation does not support fuzzing (Only building repro executables)", .{}); } else if (is_windows) { - std.log.warn("Windows does not support fuzzing (Only building repro executables)", .{}); + // Windows does not support fuzzing - only build repro executables } else if (use_system_afl) { // If we have system afl, no need for llvm-config. build_afl = true; @@ -251,6 +2361,7 @@ pub fn build(b: *std.Build) void { build_afl, use_system_afl, no_bin, + run_args, target, optimize, roc_modules, @@ -262,12 +2373,61 @@ pub fn build(b: *std.Build) void { const ModuleTest = modules.ModuleTest; +fn discoverBuiltinRocFiles(b: *std.Build) ![]const []const u8 { + const builtin_roc_path = try b.build_root.join(b.allocator, &.{ "src", "build", "roc" }); + var builtin_roc_dir = try std.fs.openDirAbsolute(builtin_roc_path, .{ .iterate = true }); + defer builtin_roc_dir.close(); + + var roc_files = std.ArrayList([]const u8).empty; + errdefer roc_files.deinit(b.allocator); + + var iter = builtin_roc_dir.iterate(); + while (try iter.next()) |entry| { + if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".roc")) { + const full_path = b.fmt("src/build/roc/{s}", .{entry.name}); + try roc_files.append(b.allocator, full_path); + } + } + + return roc_files.toOwnedSlice(b.allocator); +} + +fn generateCompiledBuiltinsSource(b: *std.Build, roc_files: []const []const u8) ![]const u8 { + var builtins_source = std.ArrayList(u8).empty; + errdefer builtins_source.deinit(b.allocator); + const writer = builtins_source.writer(b.allocator); + + for (roc_files) |roc_path| { + const roc_basename = std.fs.path.basename(roc_path); + const name_without_ext = roc_basename[0 .. roc_basename.len - 4]; + // Use lowercase with underscore for the identifier + const lower_name = try std.ascii.allocLowerString(b.allocator, name_without_ext); + + try writer.print("pub const {s}_bin = @embedFile(\"{s}.bin\");\n", .{ + lower_name, + name_without_ext, + }); + + // Also embed the source .roc file + try writer.print("pub const {s}_source = @embedFile(\"{s}\");\n", .{ + lower_name, + roc_basename, + }); + } + + // Also embed builtin_indices.bin + try writer.writeAll("pub const builtin_indices_bin = @embedFile(\"builtin_indices.bin\");\n"); + + return builtins_source.toOwnedSlice(b.allocator); +} + fn add_fuzz_target( b: *std.Build, fuzz: bool, build_afl: bool, use_system_afl: bool, no_bin: bool, + run_args: []const []const u8, target: ResolvedTarget, optimize: OptimizeMode, roc_modules: modules.RocModules, @@ -279,11 +2439,18 @@ fn add_fuzz_target( const root_source_file = b.path(b.fmt("test/fuzzing/fuzz-{s}.zig", .{name})); const fuzz_obj = b.addObject(.{ .name = b.fmt("{s}_obj", .{name}), - .root_source_file = root_source_file, - .target = target, - // Work around instrumentation bugs on mac without giving up perf on linux. - .optimize = if (target.result.os.tag == .macos) .Debug else .ReleaseSafe, + .root_module = b.createModule(.{ + .root_source_file = root_source_file, + .target = target, + // Work around instrumentation bugs on mac without giving up perf on linux. + .optimize = if (target.result.os.tag == .macos) .Debug else .ReleaseSafe, + }), }); + configureBackend(fuzz_obj, target); + // Required for fuzzing. + fuzz_obj.root_module.link_libc = true; + fuzz_obj.root_module.stack_check = false; + roc_modules.addAll(fuzz_obj); add_tracy(b, roc_modules.build_options, fuzz_obj, target, false, tracy); @@ -292,21 +2459,24 @@ fn add_fuzz_target( const repro_step = b.step(name_repro, b.fmt("run fuzz reproduction for {s}", .{name})); const repro_exe = b.addExecutable(.{ .name = name_repro, - .root_source_file = b.path("test/fuzzing/fuzz-repro.zig"), - .target = target, - .optimize = optimize, - .link_libc = true, + .root_module = b.createModule(.{ + .root_source_file = b.path("test/fuzzing/fuzz-repro.zig"), + .target = target, + .optimize = optimize, + .link_libc = true, + }), }); + configureBackend(repro_exe, target); repro_exe.root_module.addImport("fuzz_test", fuzz_obj.root_module); - install_and_run(b, no_bin, repro_exe, repro_step, repro_step); + install_and_run(b, no_bin, repro_exe, repro_step, repro_step, run_args); if (fuzz and build_afl and !no_bin) { const fuzz_step = b.step(name_exe, b.fmt("Generate fuzz executable for {s}", .{name})); b.default_step.dependOn(fuzz_step); const afl = b.lazyImport(@This(), "afl_kit") orelse return; - const fuzz_exe = afl.addInstrumentedExe(b, target, .ReleaseSafe, &.{}, use_system_afl, fuzz_obj) orelse return; + const fuzz_exe = afl.addInstrumentedExe(b, target, .ReleaseSafe, &.{}, use_system_afl, fuzz_obj, &.{"-lm"}) orelse return; const install_fuzz = b.addInstallBinFile(fuzz_exe, name_exe); fuzz_step.dependOn(&install_fuzz.step); b.getInstallStep().dependOn(&install_fuzz.step); @@ -318,103 +2488,205 @@ fn addMainExe( roc_modules: modules.RocModules, target: ResolvedTarget, optimize: OptimizeMode, - strip: ?bool, + strip: bool, + omit_frame_pointer: ?bool, enable_llvm: bool, use_system_llvm: bool, user_llvm_path: ?[]const u8, tracy: ?[]const u8, zstd: *Dependency, + compiled_builtins_module: *std.Build.Module, + write_compiled_builtins: *Step.WriteFile, + flag_enable_tracy: ?[]const u8, ) ?*Step.Compile { const exe = b.addExecutable(.{ .name = "roc", - .root_source_file = b.path("src/cli/main.zig"), - .target = target, - .optimize = optimize, - .strip = strip, - .link_libc = true, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/cli/main.zig"), + .target = target, + .optimize = optimize, + .strip = strip, + .omit_frame_pointer = omit_frame_pointer, + .link_libc = true, + }), }); + configureBackend(exe, target); - // Create test platform host static library (str) - const test_platform_host_lib = b.addStaticLibrary(.{ - .name = "test_platform_str_host", - .root_source_file = b.path("test/str/platform/host.zig"), - .target = target, - .optimize = optimize, - .strip = true, - .pic = true, // Enable Position Independent Code for PIE compatibility - }); - test_platform_host_lib.linkLibC(); - test_platform_host_lib.root_module.addImport("builtins", roc_modules.builtins); - // Force bundle compiler-rt to resolve runtime symbols like __main - test_platform_host_lib.bundle_compiler_rt = true; + // Build str and int test platform host libraries for native target + // (fx and fx-open are only built via test-platforms step) + const main_build_platforms = [_][]const u8{ "str", "int" }; + const native_target_name = roc_target.RocTarget.fromStdTarget(target.result).toName(); - // Copy the test platform host library to the source directory - const copy_test_host = b.addUpdateSourceFiles(); - const test_host_filename = if (target.result.os.tag == .windows) "host.lib" else "libhost.a"; - copy_test_host.addCopyFileToSource(test_platform_host_lib.getEmittedBin(), b.pathJoin(&.{ "test/str/platform", test_host_filename })); - b.getInstallStep().dependOn(©_test_host.step); + for (main_build_platforms) |platform_dir| { + const copy_step = buildAndCopyTestPlatformHostLib( + b, + platform_dir, + target, + native_target_name, + optimize, + roc_modules, + strip, + omit_frame_pointer, + ); + b.getInstallStep().dependOn(copy_step); + } - // Create test platform host static library (int) - const test_platform_int_host_lib = b.addStaticLibrary(.{ - .name = "test_platform_int_host", - .root_source_file = b.path("test/int/platform/host.zig"), - .target = target, - .optimize = optimize, - .strip = true, - .pic = true, // Enable Position Independent Code for PIE compatibility - }); - test_platform_int_host_lib.linkLibC(); - test_platform_int_host_lib.root_module.addImport("builtins", roc_modules.builtins); + // Cross-compile for all Linux targets (musl + glibc) + for (linux_cross_targets) |cross_target| { + const cross_resolved_target = b.resolveTargetQuery(cross_target.query); - // Copy the int test platform host library to the source directory - const copy_test_int_host = b.addUpdateSourceFiles(); - const test_int_host_filename = if (target.result.os.tag == .windows) "host.lib" else "libhost.a"; - copy_test_int_host.addCopyFileToSource(test_platform_int_host_lib.getEmittedBin(), b.pathJoin(&.{ "test/int/platform", test_int_host_filename })); - b.getInstallStep().dependOn(©_test_int_host.step); + for (main_build_platforms) |platform_dir| { + const copy_step = buildAndCopyTestPlatformHostLib( + b, + platform_dir, + cross_resolved_target, + cross_target.name, + optimize, + roc_modules, + strip, + omit_frame_pointer, + ); + b.getInstallStep().dependOn(copy_step); + } + + // Generate glibc stubs for gnu targets + if (cross_target.query.abi == .gnu) { + const glibc_stub = generateGlibcStub(b, cross_resolved_target, cross_target.name); + if (glibc_stub) |stub| { + b.getInstallStep().dependOn(&stub.step); + } + } + } // Create builtins static library at build time with minimal dependencies - const builtins_lib = b.addStaticLibrary(.{ + const builtins_obj = b.addObject(.{ .name = "roc_builtins", - .root_source_file = b.path("src/builtins/static_lib.zig"), - .target = target, - .optimize = optimize, - .strip = strip, - .pic = true, // Enable Position Independent Code for PIE compatibility + .root_module = b.createModule(.{ + .root_source_file = b.path("src/builtins/static_lib.zig"), + .target = target, + .optimize = optimize, + .strip = strip, + .omit_frame_pointer = omit_frame_pointer, + .pic = true, // Enable Position Independent Code for PIE compatibility + }), }); - // Add the builtins module so it can import "builtins" - builtins_lib.root_module.addImport("builtins", roc_modules.builtins); - // Force bundle compiler-rt to resolve math symbols - builtins_lib.bundle_compiler_rt = true; + configureBackend(builtins_obj, target); - // Create shim static library at build time - const shim_lib = b.addStaticLibrary(.{ - .name = "roc_shim", - .root_source_file = b.path("src/interpreter_shim/main.zig"), - .target = target, - .optimize = optimize, - .strip = strip, - .pic = true, // Enable Position Independent Code for PIE compatibility + // Create shim static library at build time - fully static without libc + // + // NOTE we do NOT link libC here to avoid dynamic dependency on libC + const shim_lib = b.addLibrary(.{ + .name = "roc_interpreter_shim", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/interpreter_shim/main.zig"), + .target = target, + .optimize = optimize, + .strip = strip, + .omit_frame_pointer = omit_frame_pointer, + .pic = true, // Enable Position Independent Code for PIE compatibility + }), + .linkage = .static, }); - shim_lib.linkLibC(); + configureBackend(shim_lib, target); // Add all modules from roc_modules that the shim needs roc_modules.addAll(shim_lib); + // Add compiled builtins module for loading builtin types + shim_lib.root_module.addImport("compiled_builtins", compiled_builtins_module); + shim_lib.step.dependOn(&write_compiled_builtins.step); // Link against the pre-built builtins library - shim_lib.linkLibrary(builtins_lib); - // Force bundle compiler-rt to resolve math symbols + shim_lib.addObject(builtins_obj); + // Bundle compiler-rt for our math symbols shim_lib.bundle_compiler_rt = true; - // Install shim library to the output directory const install_shim = b.addInstallArtifact(shim_lib, .{}); b.getInstallStep().dependOn(&install_shim.step); - - // We need to copy the shim library to the src/ directory for embedding as binary data + // Copy the shim library to the src/ directory for embedding as binary data // This is because @embedFile happens at compile time and needs the file to exist already // and zig doesn't permit embedding files from directories outside the source tree. const copy_shim = b.addUpdateSourceFiles(); - const shim_filename = if (target.result.os.tag == .windows) "roc_shim.lib" else "libroc_shim.a"; - copy_shim.addCopyFileToSource(shim_lib.getEmittedBin(), b.pathJoin(&.{ "src/cli", shim_filename })); + const interpreter_shim_filename = if (target.result.os.tag == .windows) "roc_interpreter_shim.lib" else "libroc_interpreter_shim.a"; + copy_shim.addCopyFileToSource(shim_lib.getEmittedBin(), b.pathJoin(&.{ "src/cli", interpreter_shim_filename })); exe.step.dependOn(©_shim.step); + // Add tracy support (required by parse/can/check modules) + add_tracy(b, roc_modules.build_options, shim_lib, b.graph.host, false, flag_enable_tracy); + + // Cross-compile interpreter shim for all supported targets + // This allows `roc build --target=X` to work for cross-compilation + const cross_compile_shim_targets = [_]struct { name: []const u8, query: std.Target.Query }{ + .{ .name = "x64musl", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl } }, + .{ .name = "arm64musl", .query = .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl } }, + .{ .name = "x64glibc", .query = .{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .gnu } }, + .{ .name = "arm64glibc", .query = .{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .gnu } }, + .{ .name = "wasm32", .query = .{ .cpu_arch = .wasm32, .os_tag = .freestanding, .abi = .none } }, + }; + + for (cross_compile_shim_targets) |cross_target| { + const cross_resolved_target = b.resolveTargetQuery(cross_target.query); + + // Build builtins object for this target + const cross_builtins_obj = b.addObject(.{ + .name = b.fmt("roc_builtins_{s}", .{cross_target.name}), + .root_module = b.createModule(.{ + .root_source_file = b.path("src/builtins/static_lib.zig"), + .target = cross_resolved_target, + .optimize = optimize, + .strip = strip, + .omit_frame_pointer = omit_frame_pointer, + .pic = true, + }), + }); + configureBackend(cross_builtins_obj, cross_resolved_target); + + // Build interpreter shim library for this target + const cross_shim_lib = b.addLibrary(.{ + .name = b.fmt("roc_interpreter_shim_{s}", .{cross_target.name}), + .root_module = b.createModule(.{ + .root_source_file = b.path("src/interpreter_shim/main.zig"), + .target = cross_resolved_target, + .optimize = optimize, + .strip = strip, + .omit_frame_pointer = omit_frame_pointer, + .pic = true, + }), + .linkage = .static, + }); + configureBackend(cross_shim_lib, cross_resolved_target); + + // For wasm32, only add the modules needed by the interpreter shim + // (compile, watch, lsp, repl, ipc use threading/file I/O not available on freestanding) + if (cross_target.query.cpu_arch == .wasm32 and cross_target.query.os_tag == .freestanding) { + cross_shim_lib.root_module.addImport("base", roc_modules.base); + cross_shim_lib.root_module.addImport("collections", roc_modules.collections); + cross_shim_lib.root_module.addImport("types", roc_modules.types); + cross_shim_lib.root_module.addImport("builtins", roc_modules.builtins); + cross_shim_lib.root_module.addImport("can", roc_modules.can); + cross_shim_lib.root_module.addImport("check", roc_modules.check); + cross_shim_lib.root_module.addImport("parse", roc_modules.parse); + cross_shim_lib.root_module.addImport("layout", roc_modules.layout); + cross_shim_lib.root_module.addImport("eval", roc_modules.eval); + cross_shim_lib.root_module.addImport("reporting", roc_modules.reporting); + cross_shim_lib.root_module.addImport("tracy", roc_modules.tracy); + cross_shim_lib.root_module.addImport("build_options", roc_modules.build_options); + // Note: ipc module is NOT added for wasm32-freestanding as it uses POSIX calls + // The interpreter shim main.zig has a stub for wasm32 + } else { + roc_modules.addAll(cross_shim_lib); + } + cross_shim_lib.root_module.addImport("compiled_builtins", compiled_builtins_module); + cross_shim_lib.step.dependOn(&write_compiled_builtins.step); + cross_shim_lib.addObject(cross_builtins_obj); + cross_shim_lib.bundle_compiler_rt = true; + + // Copy to target-specific directory for embedding + const copy_cross_shim = b.addUpdateSourceFiles(); + copy_cross_shim.addCopyFileToSource( + cross_shim_lib.getEmittedBin(), + b.pathJoin(&.{ "src/cli/targets", cross_target.name, "libroc_interpreter_shim.a" }), + ); + exe.step.dependOn(©_cross_shim.step); + } + const config = b.addOptions(); config.addOption(bool, "llvm", enable_llvm); exe.root_module.addOptions("config", config); @@ -441,6 +2713,7 @@ fn install_and_run( exe: *Step.Compile, build_step: *Step, run_step: *Step, + run_args: []const []const u8, ) void { if (run_step != build_step) { run_step.dependOn(build_step); @@ -451,18 +2724,80 @@ fn install_and_run( b.getInstallStep().dependOn(&exe.step); } else { const install = b.addInstallArtifact(exe, .{}); - build_step.dependOn(&install.step); + + // Add a step to print success message after build completes + const success_step = PrintBuildSuccessStep.create(b); + success_step.step.dependOn(&install.step); + build_step.dependOn(&success_step.step); + b.getInstallStep().dependOn(&install.step); const run = b.addRunArtifact(exe); run.step.dependOn(&install.step); - if (b.args) |args| { - run.addArgs(args); + if (run_args.len != 0) { + run.addArgs(run_args); } run_step.dependOn(&run.step); } } +const ParsedBuildArgs = struct { + run_args: []const []const u8, + test_filters: []const []const u8, +}; + +fn appendFilter( + list: *std.ArrayList([]const u8), + b: *std.Build, + value: []const u8, +) void { + const trimmed = std.mem.trim(u8, value, " \t\n\r"); + if (trimmed.len == 0) return; + list.append(b.allocator, b.dupe(trimmed)) catch @panic("OOM while parsing --test-filter value"); +} + +fn parseBuildArgs(b: *std.Build) ParsedBuildArgs { + const raw_args = b.args orelse return .{ + .run_args = &.{}, + .test_filters = &.{}, + }; + + var run_args_list = std.ArrayList([]const u8).empty; + var filter_list = std.ArrayList([]const u8).empty; + + var i: usize = 0; + while (i < raw_args.len) { + const arg = raw_args[i]; + + if (std.mem.eql(u8, arg, "--test-filter")) { + i += 1; + if (i >= raw_args.len) { + std.log.warn("ignoring --test-filter with no value", .{}); + break; + } + const value = raw_args[i]; + appendFilter(&filter_list, b, value); + i += 1; + continue; + } + + if (std.mem.startsWith(u8, arg, "--test-filter=")) { + const value = arg["--test-filter=".len..]; + appendFilter(&filter_list, b, value); + i += 1; + continue; + } + + run_args_list.append(b.allocator, arg) catch @panic("OOM while recording build arguments"); + i += 1; + } + + const run_args = run_args_list.toOwnedSlice(b.allocator) catch @panic("OOM while finalizing build arguments"); + const test_filters = filter_list.toOwnedSlice(b.allocator) catch @panic("OOM while finalizing test filters"); + + return .{ .run_args = run_args, .test_filters = test_filters }; +} + fn add_tracy( b: *std.Build, module_build_options: *std.Build.Module, @@ -599,9 +2934,8 @@ fn addStaticLlvmOptionsToModule(mod: *std.Build.Module) !void { mod.linkSystemLibrary("z", link_static); if (mod.resolved_target.?.result.os.tag != .windows or mod.resolved_target.?.result.abi != .msvc) { - // TODO: Can this just be `mod.link_libcpp = true`? Does that make a difference? - // This means we rely on clang-or-zig-built LLVM, Clang, LLD libraries. - mod.linkSystemLibrary("c++", .{}); + // Use Zig's bundled static libc++ to keep the binary statically linked + mod.link_libcpp = true; } if (mod.resolved_target.?.result.os.tag == .windows) { @@ -695,6 +3029,10 @@ const llvm_libs = [_][]const u8{ "LLVMNVPTXCodeGen", "LLVMNVPTXDesc", "LLVMNVPTXInfo", + "LLVMSPIRVAnalysis", + "LLVMSPIRVCodeGen", + "LLVMSPIRVDesc", + "LLVMSPIRVInfo", "LLVMMSP430Disassembler", "LLVMMSP430AsmParser", "LLVMMSP430CodeGen", @@ -769,14 +3107,17 @@ const llvm_libs = [_][]const u8{ "LLVMMCDisassembler", "LLVMLTO", "LLVMPasses", + "LLVMCGData", "LLVMHipStdPar", "LLVMCFGuard", "LLVMCoroutines", + "LLVMSandboxIR", "LLVMipo", "LLVMVectorize", "LLVMLinker", "LLVMInstrumentation", "LLVMFrontendOpenMP", + "LLVMFrontendAtomic", "LLVMFrontendOffloading", "LLVMFrontendOpenACC", "LLVMFrontendHLSL", @@ -861,3 +3202,146 @@ fn getCompilerVersion(b: *std.Build, optimize: OptimizeMode) []const u8 { // Git not available or failed, use fallback return std.fmt.allocPrint(b.allocator, "{s}-no-git", .{build_mode}) catch build_mode; } + +/// Generate glibc stubs at build time for cross-compilation +/// +/// This is a minimal implementation that generates essential symbols needed for basic +/// cross-compilation to glibc targets. It creates assembly stubs with required symbols +/// like __libc_start_main, abort, getauxval, and _IO_stdin_used. +/// +/// Future work: Parse Zig's abilists file to generate comprehensive +/// symbol coverage with proper versioning (e.g., symbol@@GLIBC_2.17). The abilists +/// contains thousands of glibc symbols across different versions and architectures +/// that could provide more complete stub coverage for complex applications. +fn generateGlibcStub(b: *std.Build, target: ResolvedTarget, target_name: []const u8) ?*Step.UpdateSourceFiles { + + // Generate assembly stub with comprehensive symbols using the new build module + var assembly_buf = std.ArrayList(u8).empty; + defer assembly_buf.deinit(b.allocator); + + const writer = assembly_buf.writer(b.allocator); + const target_arch = target.result.cpu.arch; + + glibc_stub_build.generateComprehensiveStub(writer, target_arch) catch |err| { + std.log.warn("Failed to generate comprehensive stub assembly for {s}: {}, using minimal ELF", .{ target_name, err }); + // Fall back to minimal ELF + const stub_content = switch (target.result.cpu.arch) { + .aarch64 => createMinimalElfArm64(), + .x86_64 => createMinimalElfX64(), + else => return null, + }; + + const write_stub = b.addWriteFiles(); + const libc_so_6 = write_stub.add("libc.so.6", stub_content); + const libc_so = write_stub.add("libc.so", stub_content); + + const copy_stubs = b.addUpdateSourceFiles(); + // Platforms that need glibc stubs + const glibc_platforms = [_][]const u8{ "int", "str" }; + for (glibc_platforms) |platform| { + copy_stubs.addCopyFileToSource(libc_so_6, b.pathJoin(&.{ "test", platform, "platform/targets", target_name, "libc.so.6" })); + copy_stubs.addCopyFileToSource(libc_so, b.pathJoin(&.{ "test", platform, "platform/targets", target_name, "libc.so" })); + } + copy_stubs.step.dependOn(&write_stub.step); + + return copy_stubs; + }; + + // Write the assembly file to the targets directory + const write_stub = b.addWriteFiles(); + const asm_file = write_stub.add("libc_stub.s", assembly_buf.items); + + // Compile the assembly into a proper shared library using Zig's build system + const libc_stub = glibc_stub_build.compileAssemblyStub(b, asm_file, target, .ReleaseSmall); + + // Copy the generated files to all platforms that use glibc targets + const copy_stubs = b.addUpdateSourceFiles(); + + // Platforms that need glibc stubs (have glibc targets defined in their .roc files) + const glibc_platforms = [_][]const u8{ "int", "str" }; + for (glibc_platforms) |platform| { + copy_stubs.addCopyFileToSource(libc_stub.getEmittedBin(), b.pathJoin(&.{ "test", platform, "platform/targets", target_name, "libc.so.6" })); + copy_stubs.addCopyFileToSource(libc_stub.getEmittedBin(), b.pathJoin(&.{ "test", platform, "platform/targets", target_name, "libc.so" })); + copy_stubs.addCopyFileToSource(asm_file, b.pathJoin(&.{ "test", platform, "platform/targets", target_name, "libc_stub.s" })); + } + copy_stubs.step.dependOn(&libc_stub.step); + copy_stubs.step.dependOn(&write_stub.step); + + return copy_stubs; +} + +/// Create a minimal ELF shared object for ARM64 +fn createMinimalElfArm64() []const u8 { + // ARM64 minimal ELF shared object + return &[_]u8{ + // ELF Header (64 bytes) + 0x7F, 'E', 'L', 'F', // e_ident[EI_MAG0..3] - ELF magic + 2, // e_ident[EI_CLASS] - ELFCLASS64 + 1, // e_ident[EI_DATA] - ELFDATA2LSB (little endian) + 1, // e_ident[EI_VERSION] - EV_CURRENT + 0, // e_ident[EI_OSABI] - ELFOSABI_NONE + 0, // e_ident[EI_ABIVERSION] + 0, 0, 0, 0, 0, 0, 0, // e_ident[EI_PAD] - padding + 0x03, 0x00, // e_type - ET_DYN (shared object) + 0xB7, 0x00, // e_machine - EM_AARCH64 + 0x01, 0x00, 0x00, 0x00, // e_version - EV_CURRENT + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_entry (not used for shared obj) + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_phoff - program header offset + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_shoff - section header offset + 0x00, 0x00, 0x00, 0x00, // e_flags + 0x40, 0x00, // e_ehsize - ELF header size + 0x38, 0x00, // e_phentsize - program header entry size + 0x01, 0x00, // e_phnum - number of program headers + 0x40, 0x00, // e_shentsize - section header entry size + 0x00, 0x00, // e_shnum - number of section headers + 0x00, 0x00, // e_shstrndx - section header string table index + + // Program Header (56 bytes) - PT_LOAD + 0x01, 0x00, 0x00, 0x00, // p_type - PT_LOAD + 0x05, 0x00, 0x00, 0x00, // p_flags - PF_R | PF_X + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_offset + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_vaddr + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_paddr + 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_filesz + 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_memsz + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_align + }; +} + +/// Create a minimal ELF shared object for x86-64 +fn createMinimalElfX64() []const u8 { + // x86-64 minimal ELF shared object + return &[_]u8{ + // ELF Header (64 bytes) + 0x7F, 'E', 'L', 'F', // e_ident[EI_MAG0..3] - ELF magic + 2, // e_ident[EI_CLASS] - ELFCLASS64 + 1, // e_ident[EI_DATA] - ELFDATA2LSB (little endian) + 1, // e_ident[EI_VERSION] - EV_CURRENT + 0, // e_ident[EI_OSABI] - ELFOSABI_NONE + 0, // e_ident[EI_ABIVERSION] + 0, 0, 0, 0, 0, 0, 0, // e_ident[EI_PAD] - padding + 0x03, 0x00, // e_type - ET_DYN (shared object) + 0x3E, 0x00, // e_machine - EM_X86_64 + 0x01, 0x00, 0x00, 0x00, // e_version - EV_CURRENT + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_entry (not used for shared obj) + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_phoff - program header offset + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // e_shoff - section header offset + 0x00, 0x00, 0x00, 0x00, // e_flags + 0x40, 0x00, // e_ehsize - ELF header size + 0x38, 0x00, // e_phentsize - program header entry size + 0x01, 0x00, // e_phnum - number of program headers + 0x40, 0x00, // e_shentsize - section header entry size + 0x00, 0x00, // e_shnum - number of section headers + 0x00, 0x00, // e_shstrndx - section header string table index + + // Program Header (56 bytes) - PT_LOAD + 0x01, 0x00, 0x00, 0x00, // p_type - PT_LOAD + 0x05, 0x00, 0x00, 0x00, // p_flags - PF_R | PF_X + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_offset + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_vaddr + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_paddr + 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_filesz + 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_memsz + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // p_align + }; +} diff --git a/build.zig.zon b/build.zig.zon index 7d4b415962..d6472c94e0 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,59 +1,59 @@ .{ .name = .roc, .version = "0.0.0", - .minimum_zig_version = "0.14.0", + .minimum_zig_version = "0.15.2", .dependencies = .{ .afl_kit = .{ - .url = "git+https://github.com/bhansconnect/zig-afl-kit?ref=zig-0.14.0#9f09f8e5c29102b94d775305801315320515a2b8", - .hash = "afl_kit-0.1.0-uhOgGEEbAADSSVtFLWc0eoZFxVLiELWLNldB9K_f9x5L", + .url = "git+https://github.com/bhansconnect/zig-afl-kit?ref=main#b863c41ca47ed05729e0b509fb1926c111aa2800", + .hash = "afl_kit-0.1.0-NdJ3cncdAAA4154gtkRqNApovBYfOs-LWADNE-9BzzPC", .lazy = true, }, .roc_deps_aarch64_macos_none = .{ - .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.13.0/aarch64-macos-none.tar.xz?response-content-disposition=attachment", - .hash = "122044a065bfe8f6286901b110a1b0b5a764f9fcb2d1472a5eeb3423527e95419427", + .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.15.1/aarch64-macos-none.tar.xz?response-content-disposition=attachment", + .hash = "N-V-__8AAJuttw4mNdQg3ig107ac4uyAhcFPznGHmpnmX58C", .lazy = true, }, .roc_deps_aarch64_linux_musl = .{ - .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.13.0/aarch64-linux-musl.tar.xz?response-content-disposition=attachment", - .hash = "1220949c8b509cbdac3eb9de5656906ea3e777dfea9e7333f32462754cb1d7708bf2", + .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.15.1/aarch64-linux-musl.tar.xz?response-content-disposition=attachment", + .hash = "N-V-__8AABnBVRNhZGWHvWKm8PO-N4Js4Zr65NnswmkZ0nYX", .lazy = true, }, .roc_deps_aarch64_windows_gnu = .{ - .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.13.0/aarch64-windows-gnu.zip?response-content-disposition=attachment", - .hash = "1220fcd7dcb6768b907f20369ec6390adb4de41bd5c1c34dc0f1611af50a331b3e01", + .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.15.1/aarch64-windows-gnu.zip?response-content-disposition=attachment", + .hash = "N-V-__8AAEbXoBTC007kkcMVW2_P5yIKMxPKQ-L5sYEc3_qH", .lazy = true, }, .roc_deps_arm_linux_musleabihf = .{ - .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.13.0/arm-linux-musleabihf.tar.xz?response-content-disposition=attachment", - .hash = "122037ecc8654feb3d34da5424ca6a73f140dbd30475e3d4f23a6f29844e799d51a3", + .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.15.1/arm-linux-musleabihf.tar.xz?response-content-disposition=attachment", + .hash = "N-V-__8AAE9SyhMGHGnkgRenWYw-birLp2Nl-IYGqIbdlga3", .lazy = true, }, .roc_deps_x86_linux_musl = .{ - .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.13.0/x86-linux-musl.tar.xz?response-content-disposition=attachment", - .hash = "12208fbefa56ba6571399c60d96810d00a561af1926cf566e07fc32c748fbbb70ccf", + .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.15.1/x86-linux-musl.tar.xz?response-content-disposition=attachment", + .hash = "N-V-__8AAGXNmxEQQYT5QBEheV2NJzSQjwaBuUx8wj_tGdoy", .lazy = true, }, .roc_deps_x86_64_linux_musl = .{ - .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.13.0/x86_64-linux-musl.tar.xz?response-content-disposition=attachment", - .hash = "1220aff2ba681359149edde57256206d87695f84d33375082a3a442da9ba8dfc177f", + .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.15.1/x86_64-linux-musl.tar.xz?response-content-disposition=attachment", + .hash = "N-V-__8AAL1yjxS0Lef6Fv5mMGaqNa0rGcPJxOftYK0NYuJu", .lazy = true, }, .roc_deps_x86_64_macos_none = .{ - .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.13.0/x86_64-macos-none.tar.xz?response-content-disposition=attachment", - .hash = "1220a0714d1cd12799885b5217252511012cca5d2d679d318520515d2487b80533db", + .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.15.1/x86_64-macos-none.tar.xz?response-content-disposition=attachment", + .hash = "N-V-__8AAInnSA9gFeMzlB67m7Nu-NYBUOXqDrzYmYgatUHk", .lazy = true, }, .roc_deps_x86_64_windows_gnu = .{ - .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.13.0/x86_64-windows-gnu.zip?response-content-disposition=attachment", - .hash = "122070972a9f996fc1a167b78d3a6a8c10f85349f9369383f5e26af1c5eed3cc41b0", + .url = "https://github.com/roc-lang/roc-bootstrap/releases/download/zig-0.15.1/x86_64-windows-gnu.zip?response-content-disposition=attachment", + .hash = "N-V-__8AANpEpBfszYPGDvz9XJK8VRBNG7eQzzK1iNSlkdVG", .lazy = true, }, .bytebox = .{ - .url = "git+https://github.com/lukewilliamboswell/bytebox.git#694b53b748dba8078751a8836f1b3cead792dcca", - .hash = "bytebox-0.0.1-SXc2sSfbDgCLYYKaTGdlU4Dl3ncfvesPax1bkLHAlCg6", + .url = "git+https://github.com/rdunnington/bytebox#5c8753ba11c394e4d642dddbb459edcd7c97ac26", + .hash = "bytebox-0.0.1-SXc2seA2DwAUHbrqTMz_mAQQGqO0EVPYmZ89YZn4KsTi", }, .zstd = .{ - .url = "git+https://github.com/allyourcodebase/zstd.git#1.5.7", + .url = "git+https://github.com/allyourcodebase/zstd.git#01327d49cbc56dc24c20a167bb0055d7fc23de84", // 1.5.7 .hash = "1220b9feb4652a62df95843e78a5db008401599366989b52d7cab421bf6263fa73d0", }, }, diff --git a/ci/check_test_wiring.zig b/ci/check_test_wiring.zig new file mode 100644 index 0000000000..4ff743610c --- /dev/null +++ b/ci/check_test_wiring.zig @@ -0,0 +1,417 @@ +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const Ast = std.zig.Ast; +const PathList = std.ArrayList([]u8); + +const max_file_bytes: usize = 16 * 1024 * 1024; + +const test_file_exclusions = [_][]const u8{ + // TODO Fixing in progress... + "src/cli/test_docs.zig", +}; + +const TermColor = struct { + pub const red = "\x1b[0;31m"; + pub const green = "\x1b[0;32m"; + pub const yellow = "\x1b[1;33m"; + pub const reset = "\x1b[0m"; +}; + +pub fn main() !void { + var gpa_impl = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa_impl.deinit(); + const gpa = gpa_impl.allocator(); + + var stdout_buffer: [4096]u8 = undefined; + var stdout_state = std.fs.File.stdout().writer(&stdout_buffer); + const stdout = &stdout_state.interface; + + try stdout.print("Checking test wiring in src/ directory...\n\n", .{}); + + try stdout.print("Step 1: Finding all potential test files...\n", .{}); + var test_files = PathList{}; + defer freePathList(&test_files, gpa); + + var mod_files = PathList{}; + defer freePathList(&mod_files, gpa); + + try walkTree(gpa, "src", &test_files, &mod_files); + try stdout.print("Found {d} potential test files\n\n", .{test_files.items.len}); + + // Some tests are wired through build.zig rather than mod.zig files. + // For example, the CLI tests are driven via src/cli/main.zig and + // src/cli/test/roc_subcommands.zig test roots. + // + // To avoid false positives, we: + // - Treat src/cli/main.zig as an additional aggregator when scanning @import() + // statements for wired test files. + // - Treat src/cli/test/fx_platform_test.zig as an aggregator since it imports + // fx_test_specs.zig which contains shared test specifications. + if (fileExists("src/cli/main.zig")) { + try mod_files.append(gpa, try gpa.dupe(u8, "src/cli/main.zig")); + } + if (fileExists("src/cli/test/fx_platform_test.zig")) { + try mod_files.append(gpa, try gpa.dupe(u8, "src/cli/test/fx_platform_test.zig")); + } + if (fileExists("src/cli/test/test_runner.zig")) { + try mod_files.append(gpa, try gpa.dupe(u8, "src/cli/test/test_runner.zig")); + } + if (fileExists("src/cli/cli_error.zig")) { + try mod_files.append(gpa, try gpa.dupe(u8, "src/cli/cli_error.zig")); + } + + if (test_files.items.len == 0) { + try stdout.print("{s}[OK]{s} No test files found to check\n", .{ TermColor.green, TermColor.reset }); + try stdout.flush(); + return; + } + + try stdout.print("Step 2: Extracting test references from mod.zig files...\n", .{}); + var referenced = std.StringHashMap(void).init(gpa); + defer { + var it = referenced.keyIterator(); + while (it.next()) |key| { + gpa.free(@constCast(key.*)); + } + referenced.deinit(); + } + + for (mod_files.items) |mod_path| { + try collectModImports(gpa, mod_path, &referenced); + } + // Also treat test roots declared in build.zig (b.addTest root_source_file) + // as valid wiring for the corresponding files (e.g. src/cli/main.zig and + // src/cli/test/roc_subcommands.zig). + try markBuildTestRootsAsReferenced(gpa, &referenced); + + try stdout.print( + "Found {d} file references in mod.zig files and build.zig test roots\n\n", + .{referenced.count()}, + ); + + try stdout.print("Step 3: Checking if all test files are properly wired...\n\n", .{}); + var unwired = PathList{}; + defer freePathList(&unwired, gpa); + + for (test_files.items) |test_path| { + const key: []const u8 = test_path; + if (!referenced.contains(key)) { + try unwired.append(gpa, try gpa.dupe(u8, key)); + } + } + + if (unwired.items.len > 0) { + std.mem.sort([]u8, unwired.items, {}, lessThanPath); + try stdout.print( + "{s}[ERR]{s} Found {d} test file(s) that are NOT wired through mod.zig:\n\n", + .{ TermColor.red, TermColor.reset, unwired.items.len }, + ); + + for (unwired.items) |path| { + const path_text: []const u8 = path; + try stdout.print(" {s}[MISSING]{s} {s}\n", .{ TermColor.red, TermColor.reset, path_text }); + try printSuggestion(gpa, stdout, path_text); + try stdout.print("\n", .{}); + } + + try stdout.print("{s}[ERR]{s} Test wiring issues found. Please fix the issues above.\n\n", .{ + TermColor.red, + TermColor.reset, + }); + try stdout.print("To fix:\n", .{}); + try stdout.print("1. Add missing std.testing.refAllDecls() calls to the appropriate mod.zig files\n", .{}); + try stdout.print("2. Ensure all modules with tests are listed in src/build/modules.zig test_configs\n\n", .{}); + } else { + try stdout.print("{s}[OK]{s} All tests are properly wired!\n\n", .{ TermColor.green, TermColor.reset }); + } + + if (unwired.items.len > 0) { + try stdout.flush(); + std.process.exit(1); + } + + try stdout.flush(); +} + +/// Normalize path separators to forward slashes for consistent cross-platform comparison. +/// This is important because: +/// 1. Zig @import paths always use forward slashes +/// 2. We need consistent path comparison between walked files and mod.zig imports +fn normalizePath(allocator: Allocator, path: []u8) ![]u8 { + if (comptime @import("builtin").os.tag == .windows) { + const normalized = try allocator.dupe(u8, path); + for (normalized) |*c| { + if (c.* == '\\') c.* = '/'; + } + allocator.free(path); + return normalized; + } + return path; +} + +fn walkTree( + allocator: Allocator, + dir_path: []const u8, + test_files: *PathList, + mod_files: *PathList, +) !void { + var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true }); + defer dir.close(); + + var it = dir.iterate(); + while (try it.next()) |entry| { + if (entry.kind == .sym_link) continue; + + const joined_path = try std.fs.path.join(allocator, &.{ dir_path, entry.name }); + const next_path = try normalizePath(allocator, joined_path); + + switch (entry.kind) { + .directory => { + defer allocator.free(next_path); + try walkTree(allocator, next_path, test_files, mod_files); + }, + .file => { + try handleFile(allocator, next_path, entry.name, test_files, mod_files); + }, + else => allocator.free(next_path), + } + } +} + +fn handleFile( + allocator: Allocator, + path: []u8, + file_name: []const u8, + test_files: *PathList, + mod_files: *PathList, +) !void { + if (!std.mem.endsWith(u8, file_name, ".zig")) { + allocator.free(path); + return; + } + + if (std.mem.eql(u8, file_name, "mod.zig")) { + try mod_files.append(allocator, path); + return; + } + + if (shouldSkipTestFile(path)) { + allocator.free(path); + return; + } + + if (try fileHasTestDecl(allocator, path)) { + try test_files.append(allocator, path); + return; + } + + allocator.free(path); +} + +fn shouldSkipTestFile(path: []const u8) bool { + for (test_file_exclusions) |excluded| { + if (std.mem.eql(u8, path, excluded)) return true; + } + return false; +} + +fn fileHasTestDecl(allocator: Allocator, path: []const u8) !bool { + const source = try readSourceFile(allocator, path); + defer allocator.free(source); + var tree = try Ast.parse(allocator, source, .zig); + defer tree.deinit(allocator); + + const tags = tree.nodes.items(.tag); + for (tags) |tag| { + if (tag == .test_decl) { + return true; + } + } + + return false; +} + +fn readSourceFile(allocator: Allocator, path: []const u8) ![:0]u8 { + return try std.fs.cwd().readFileAllocOptions( + allocator, + path, + max_file_bytes, + null, + std.mem.Alignment.of(u8), + 0, + ); +} + +fn collectModImports( + allocator: Allocator, + mod_path: []const u8, + referenced: *std.StringHashMap(void), +) !void { + const source = try readSourceFile(allocator, mod_path); + defer allocator.free(source); + + var tree = try Ast.parse(allocator, source, .zig); + defer tree.deinit(allocator); + + const tags = tree.tokens.items(.tag); + var idx: usize = 0; + while (idx < tree.tokens.len) : (idx += 1) { + if (tags[idx] != .builtin) continue; + const token_index = @as(Ast.TokenIndex, @intCast(idx)); + if (!std.mem.eql(u8, tree.tokenSlice(token_index), "@import")) continue; + + const import_path = try extractImportPath(allocator, &tree, idx) orelse continue; + defer allocator.free(import_path); + + if (!std.mem.endsWith(u8, import_path, ".zig")) continue; + + const resolved = try resolveImportPath(allocator, mod_path, import_path); + if (referenced.contains(resolved)) { + allocator.free(resolved); + } else { + try referenced.put(resolved, {}); + } + } +} + +fn extractImportPath( + allocator: Allocator, + tree: *const Ast, + builtin_token_index: usize, +) !?[]u8 { + var cursor = builtin_token_index + 1; + if (cursor >= tree.tokens.len) return null; + if (tree.tokenTag(@intCast(cursor)) != .l_paren) return null; + + cursor += 1; + if (cursor >= tree.tokens.len) return null; + const str_token_index = @as(Ast.TokenIndex, @intCast(cursor)); + const tag = tree.tokenTag(str_token_index); + if (tag != .string_literal) return null; + + const literal = tree.tokenSlice(str_token_index); + if (literal.len < 2) return null; + return try allocator.dupe(u8, literal[1 .. literal.len - 1]); +} + +fn resolveImportPath( + allocator: Allocator, + mod_path: []const u8, + import_path: []const u8, +) ![]u8 { + const mod_dir = std.fs.path.dirname(mod_path) orelse "."; + return std.fs.path.resolvePosix(allocator, &.{ mod_dir, import_path }); +} + +/// Mark files that are used as test roots in build.zig as "wired". +/// +/// In addition to mod.zig imports, some tests are hooked up via explicit +/// `b.addTest` calls in build.zig (for example the CLI tests). Any Zig +/// file that is used as a `root_source_file = b.path("...")` in such a +/// test configuration should not be reported as missing wiring. +fn markBuildTestRootsAsReferenced( + allocator: Allocator, + referenced: *std.StringHashMap(void), +) !void { + const build_path = "build.zig"; + if (!fileExists(build_path)) return; + + const source = try readSourceFile(allocator, build_path); + defer allocator.free(source); + + const pattern = ".root_source_file = b.path(\""; + var search_index: usize = 0; + + while (std.mem.indexOfPos(u8, source, search_index, pattern)) |match_pos| { + const literal_start = match_pos + pattern.len; + var cursor = literal_start; + + // Find end of the string literal. + while (cursor < source.len and source[cursor] != '"') : (cursor += 1) {} + if (cursor >= source.len) break; + + const rel_path = source[literal_start..cursor]; + + // Only consider Zig source files under src/ as potential test roots. + if (!std.mem.endsWith(u8, rel_path, ".zig")) { + search_index = cursor + 1; + continue; + } + if (!std.mem.startsWith(u8, rel_path, "src/")) { + search_index = cursor + 1; + continue; + } + + const key = try allocator.dupe(u8, rel_path); + if (referenced.contains(key)) { + allocator.free(key); + } else { + try referenced.put(key, {}); + } + + search_index = cursor + 1; + } +} + +fn lessThanPath(_: void, lhs: []u8, rhs: []u8) bool { + const l: []const u8 = lhs; + const r: []const u8 = rhs; + return std.mem.lessThan(u8, l, r); +} + +fn printSuggestion( + allocator: Allocator, + writer: anytype, + test_path: []const u8, +) !void { + const maybe_mod = try findNearestMod(allocator, test_path); + if (maybe_mod) |mod_path| { + defer allocator.free(mod_path); + + const mod_dir = std.fs.path.dirname(mod_path) orelse "."; + const relative = try std.fs.path.relativePosix(allocator, mod_dir, test_path); + defer allocator.free(relative); + + try writer.print(" {s}[HINT]{s} Should be added to {s}\n", .{ + TermColor.yellow, + TermColor.reset, + mod_path, + }); + try writer.print( + " {s}[HINT]{s} Add: std.testing.refAllDecls(@import(\"{s}\"));\n", + .{ TermColor.yellow, TermColor.reset, relative }, + ); + } else { + try writer.print( + " {s}[HINT]{s} No nearby mod.zig found for this test file\n", + .{ TermColor.yellow, TermColor.reset }, + ); + } +} + +fn findNearestMod(allocator: Allocator, file_path: []const u8) !?[]u8 { + var current_dir_opt = std.fs.path.dirname(file_path); + while (current_dir_opt) |current_dir| { + const joined = try std.fs.path.join(allocator, &.{ current_dir, "mod.zig" }); + const candidate = try normalizePath(allocator, joined); + if (fileExists(candidate)) { + return candidate; + } + allocator.free(candidate); + current_dir_opt = std.fs.path.dirname(current_dir); + } + return null; +} + +fn fileExists(path: []const u8) bool { + _ = std.fs.cwd().statFile(path) catch return false; + return true; +} + +fn freePathList(list: *PathList, allocator: Allocator) void { + for (list.items) |path| { + allocator.free(path); + } + list.deinit(allocator); +} diff --git a/ci/custom_valgrind.sh b/ci/custom_valgrind.sh new file mode 100755 index 0000000000..bd15dff6f4 --- /dev/null +++ b/ci/custom_valgrind.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# Get the directory where this script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +valgrind \ + --suppressions="$SCRIPT_DIR/valgrind.supp" \ + --leak-check=full \ + --error-exitcode=1 \ + --errors-for-leak-kinds=definite,possible \ + "$@" 2>&1 | grep -v "Warning: DWARF2 reader: Badly formed extended line op encountered" +exit ${PIPESTATUS[0]} diff --git a/ci/glossary-link-checker.roc b/ci/glossary-link-checker.roc index f3508b8d33..8f10394508 100644 --- a/ci/glossary-link-checker.roc +++ b/ci/glossary-link-checker.roc @@ -1,8 +1,8 @@ -app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" } +app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" } import cli.Stdout import cli.Stderr -import cli.Path +import cli.File import "../Glossary.md" as glossary_as_str : Str # This script checks if all markdown links that point to files or dirs are valid for the file Glossary.md @@ -81,12 +81,8 @@ check_link! = |link_str| # TODO check links to other markdown headers as well, e.g. #tokenization Ok({}) else - path = Path.from_str(link_str) + _ = File.exists!(link_str) ? |_| BadLink(link_str) - when Path.type!(path) is - Ok(_) -> - Ok({}) - Err(_) -> - Err(BadLink(link_str)) + Ok({}) diff --git a/ci/retry_flaky.sh b/ci/retry_flaky.sh deleted file mode 100755 index 480f2ffd5a..0000000000 --- a/ci/retry_flaky.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -e - -for i in {1..3}; do - output="$("$@" 2>&1)" && break - if echo "$output" | grep -q "error: bad HTTP response code: '500 Internal Server Error'"; then - echo "Retrying due to HTTP 500 error ($i/3)..." - sleep 2 - else - echo "$output" - exit 1 - fi -done \ No newline at end of file diff --git a/ci/valgrind.supp b/ci/valgrind.supp new file mode 100644 index 0000000000..90001494a7 --- /dev/null +++ b/ci/valgrind.supp @@ -0,0 +1,57 @@ +# Valgrind suppression file for Roc +# +# These suppressions address false positives from the interaction between +# LLVM and musl's mallocng allocator. The uninitialised values originate +# from LLVM code (DebugCounter::registerCounter and scc_iterator::DFSVisitChildren) +# and propagate through to musl's malloc where they trigger conditional jumps +# in the enframe function. These are safe because: +# 1. musl's mallocng is designed to handle these patterns +# 2. Fresh mmap'd memory will be zeroed, reused memory has valid values +# 3. The program runs correctly and produces correct results + +# Suppress uninitialised value errors in musl's malloc enframe function +# when called from LLVM code paths +{ + musl-mallocng-enframe-llvm-global-init + Memcheck:Cond + fun:enframe + fun:__libc_malloc_impl + ... + fun:_GLOBAL__sub_I_* +} + +{ + musl-mallocng-enframe-llvm-allocate + Memcheck:Cond + fun:enframe + fun:__libc_malloc_impl + fun:aligned_alloc + ... + fun:*llvm* +} + +{ + musl-mallocng-enframe-lld + Memcheck:Cond + fun:enframe + fun:__libc_malloc_impl + ... + fun:*lld* +} + +{ + musl-mallocng-enframe-smallvector + Memcheck:Cond + fun:enframe + fun:__libc_malloc_impl + fun:*SmallVector* +} + +{ + musl-mallocng-enframe-llvm-mcregisterinfo + Memcheck:Cond + fun:enframe + fun:__libc_malloc_impl + ... + fun:*llvm*MCRegisterInfo* +} diff --git a/ci/zig_lints.sh b/ci/zig_lints.sh deleted file mode 100755 index 9782ab81db..0000000000 --- a/ci/zig_lints.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash - -# https://vaneyckt.io/posts/safer_bash_scripts_with_set_euxo_pipefail/ -set -euo pipefail - -# Check for pub declarations without doc comments - -found_errors=false - -# Lint all zig files in src/ -while read -r file; do - errors=$(awk ' - /^pub / { - if (prev !~ /^\/\/\//) { - # Skip doc comment requirements for init, deinit, @import, and pub const re-exports - if ($0 !~ /pub.*fn init\(/ && - $0 !~ /pub.*fn deinit/ && - $0 !~ /pub.*@import/ && - $0 !~ /pub.*const.*=[[:space:]]*[a-z][^[:space:]]*\.[^([:space:]]*;/) { - print FILENAME ":" FNR ": pub declaration without doc comment `///`" - } - } - } - { prev = $0 } - ' "$file") - if [[ -n "$errors" ]]; then - echo "$errors" - found_errors=true - fi -done < <(find src -type f -name "*.zig" -not -path "*/.zig-cache/*") - -if [[ "$found_errors" == true ]]; then - echo "" - echo "Please add doc comments to the spots listed above, they make the code easier to understand for everyone." - echo "" - exit 1 -fi - -# Check for top level comments in new Zig files - -NEW_ZIG_FILES=$(git diff --name-only --diff-filter=A origin/main HEAD -- | grep 'src/' | grep '\.zig$' || echo "") - -if [ -z "$NEW_ZIG_FILES" ]; then - # No new Zig files found - exit 0 -fi - -FAILED_FILES="" - -for FILE in $NEW_ZIG_FILES; do - if ! grep -q "//!" "$FILE"; then - echo "Error: $FILE is missing top level comment (//!)" - FAILED_FILES="$FAILED_FILES $FILE" - fi -done - -if [ -n "$FAILED_FILES" ]; then - echo "" - echo "The following files are missing a top level comment:" - echo " $FAILED_FILES" - echo "" - echo "Add a //! comment BEFORE any other code that explains the purpose of the file." - exit 1 -fi diff --git a/ci/zig_lints.zig b/ci/zig_lints.zig new file mode 100644 index 0000000000..4a0f7030cf --- /dev/null +++ b/ci/zig_lints.zig @@ -0,0 +1,364 @@ +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const PathList = std.ArrayList([]u8); + +const max_file_bytes: usize = 16 * 1024 * 1024; + +const TermColor = struct { + pub const red = "\x1b[0;31m"; + pub const green = "\x1b[0;32m"; + pub const reset = "\x1b[0m"; +}; + +pub fn main() !void { + var gpa_impl = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa_impl.deinit(); + const gpa = gpa_impl.allocator(); + + var stdout_buffer: [4096]u8 = undefined; + var stdout_state = std.fs.File.stdout().writer(&stdout_buffer); + const stdout = &stdout_state.interface; + + var found_errors = false; + + // Lint 1: Check for separator comments (// ====) + try stdout.print("Checking for separator comments (// ====)...\n", .{}); + + { + var zig_files = PathList{}; + defer freePathList(&zig_files, gpa); + + // Scan src/, build.zig, and test/ (not ci/ since zig_lints.zig mentions the pattern) + try walkTree(gpa, "src", &zig_files); + try walkTree(gpa, "test", &zig_files); + + // Add build.zig directly + try zig_files.append(gpa, try gpa.dupe(u8, "build.zig")); + + for (zig_files.items) |file_path| { + const errors = try checkSeparatorComments(gpa, file_path); + defer gpa.free(errors); + + if (errors.len > 0) { + try stdout.print("{s}", .{errors}); + found_errors = true; + } + } + + if (found_errors) { + try stdout.print("\n", .{}); + try stdout.print("Separator comments like '// ====' are not allowed. Please delete these lines.\n", .{}); + try stdout.print("\n", .{}); + try stdout.flush(); + std.process.exit(1); + } + } + + // Lint 2: Check for pub declarations without doc comments + try stdout.print("Checking for pub declarations without doc comments...\n", .{}); + + var zig_files = PathList{}; + defer freePathList(&zig_files, gpa); + + try walkTree(gpa, "src", &zig_files); + + for (zig_files.items) |file_path| { + const errors = try checkPubDocComments(gpa, file_path); + defer gpa.free(errors); + + if (errors.len > 0) { + try stdout.print("{s}", .{errors}); + found_errors = true; + } + } + + if (found_errors) { + try stdout.print("\n", .{}); + try stdout.print("Please add doc comments to the spots listed above, they make the code easier to understand for everyone.\n", .{}); + try stdout.print("\n", .{}); + try stdout.flush(); + std.process.exit(1); + } + + // Lint 2: Check for top level comments in new Zig files + try stdout.print("Checking for top level comments in new Zig files...\n", .{}); + + var new_zig_files = try getNewZigFiles(gpa); + defer { + for (new_zig_files.items) |path| { + gpa.free(path); + } + new_zig_files.deinit(gpa); + } + + if (new_zig_files.items.len == 0) { + try stdout.print("{s}[OK]{s} All lints passed!\n", .{ TermColor.green, TermColor.reset }); + try stdout.flush(); + return; + } + + var failed_files = PathList{}; + defer freePathList(&failed_files, gpa); + + for (new_zig_files.items) |file_path| { + if (!try fileHasTopLevelComment(gpa, file_path)) { + try stdout.print("Error: {s} is missing top level comment (//!)\n", .{file_path}); + try failed_files.append(gpa, try gpa.dupe(u8, file_path)); + } + } + + if (failed_files.items.len > 0) { + try stdout.print("\n", .{}); + try stdout.print("The following files are missing a top level comment:\n", .{}); + for (failed_files.items) |path| { + try stdout.print(" {s}\n", .{path}); + } + try stdout.print("\n", .{}); + try stdout.print("Add a //! comment that explains the purpose of the file BEFORE any other code.\n", .{}); + try stdout.flush(); + std.process.exit(1); + } + + try stdout.print("{s}[OK]{s} All lints passed!\n", .{ TermColor.green, TermColor.reset }); + try stdout.flush(); +} + +fn walkTree(allocator: Allocator, dir_path: []const u8, zig_files: *PathList) !void { + var dir = try std.fs.cwd().openDir(dir_path, .{ .iterate = true }); + defer dir.close(); + + var it = dir.iterate(); + while (try it.next()) |entry| { + if (entry.kind == .sym_link) continue; + + const next_path = try std.fs.path.join(allocator, &.{ dir_path, entry.name }); + + switch (entry.kind) { + .directory => { + // Skip .zig-cache directories + if (std.mem.eql(u8, entry.name, ".zig-cache")) { + allocator.free(next_path); + continue; + } + defer allocator.free(next_path); + try walkTree(allocator, next_path, zig_files); + }, + .file => { + if (std.mem.endsWith(u8, entry.name, ".zig")) { + try zig_files.append(allocator, next_path); + } else { + allocator.free(next_path); + } + }, + else => allocator.free(next_path), + } + } +} + +fn checkSeparatorComments(allocator: Allocator, file_path: []const u8) ![]u8 { + const source = readSourceFile(allocator, file_path) catch |err| { + // Skip files we can't read + if (err == error.FileNotFound) return try allocator.dupe(u8, ""); + return err; + }; + defer allocator.free(source); + + var errors = std.ArrayList(u8){}; + errdefer errors.deinit(allocator); + + var line_num: usize = 1; + var lines = std.mem.splitScalar(u8, source, '\n'); + + while (lines.next()) |line| { + defer line_num += 1; + + // Trim leading whitespace + const trimmed = std.mem.trimLeft(u8, line, " \t"); + + // Check if line starts with // and is a separator comment + // Separator comments are lines like "// ====" or "// ==== Section ====" + // We detect them by checking if, after the //, the line consists only of + // whitespace, equals signs, and letters (for section titles) + if (std.mem.startsWith(u8, trimmed, "//")) { + const after_slashes = trimmed[2..]; + if (isSeparatorComment(after_slashes)) { + try errors.writer(allocator).print("{s}:{d}: separator comment '// ====' not allowed\n", .{ file_path, line_num }); + } + } + } + + return errors.toOwnedSlice(allocator); +} + +/// Checks if a line (after the //) is a separator comment. +/// Separator comments are lines consisting only of equals signs (with optional +/// whitespace and a section title between them). +/// Examples that should match: +/// " ====" +/// " ===== Section =====" +/// " ==== Title ====" +/// Examples that should NOT match: +/// " 2. Stdout contains "====="" +/// " This is a normal comment about ====" +fn isSeparatorComment(after_slashes: []const u8) bool { + // Must contain ==== + if (std.mem.indexOf(u8, after_slashes, "====") == null) return false; + + // Trim whitespace + const content = std.mem.trim(u8, after_slashes, " \t"); + if (content.len == 0) return false; + + // Must start with ==== + if (!std.mem.startsWith(u8, content, "====")) return false; + + // Find where the leading equals end + var i: usize = 0; + while (i < content.len and content[i] == '=') : (i += 1) {} + + // Everything after leading equals should be whitespace, letters, or trailing equals + while (i < content.len) : (i += 1) { + const c = content[i]; + if (c == '=' or c == ' ' or c == '\t' or (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z')) { + continue; + } + // Found a character that's not allowed in separator comments + return false; + } + + return true; +} + +fn checkPubDocComments(allocator: Allocator, file_path: []const u8) ![]u8 { + const source = readSourceFile(allocator, file_path) catch |err| { + // Skip files we can't read + if (err == error.FileNotFound) return try allocator.dupe(u8, ""); + return err; + }; + defer allocator.free(source); + + var errors = std.ArrayList(u8){}; + errdefer errors.deinit(allocator); + + var line_num: usize = 1; + var prev_line: []const u8 = ""; + var lines = std.mem.splitScalar(u8, source, '\n'); + + while (lines.next()) |line| { + defer { + prev_line = line; + line_num += 1; + } + + // Check if line starts with "pub " (no leading whitespace - only top-level declarations) + if (!std.mem.startsWith(u8, line, "pub ")) continue; + + // Check if previous line is a doc comment (allow indented doc comments) + const prev_trimmed = std.mem.trimLeft(u8, prev_line, " \t"); + if (std.mem.startsWith(u8, prev_trimmed, "///")) continue; + + // Skip exceptions: init, deinit, @import, and pub const re-exports + // Note: "pub.*fn init\(" in bash matches "init" anywhere in function name + if (std.mem.indexOf(u8, line, "fn init") != null) continue; + if (std.mem.indexOf(u8, line, "fn deinit") != null) continue; + if (std.mem.indexOf(u8, line, "@import") != null) continue; + + // Check for pub const re-exports (e.g., "pub const Foo = bar.Baz;") + if (isReExport(line)) continue; + + try errors.writer(allocator).print("{s}:{d}: pub declaration without doc comment `///`\n", .{ file_path, line_num }); + } + + return errors.toOwnedSlice(allocator); +} + +fn isReExport(line: []const u8) bool { + // Match pattern: pub const X = lowercase.something; + // This detects re-exports like "pub const Foo = bar.Baz;" + if (!std.mem.startsWith(u8, line, "pub const ")) return false; + + // Find the '=' sign + const eq_pos = std.mem.indexOf(u8, line, "=") orelse return false; + const after_eq = std.mem.trimLeft(u8, line[eq_pos + 1 ..], " \t"); + + // Check if it starts with a lowercase letter (module reference) + if (after_eq.len == 0) return false; + const first_char = after_eq[0]; + if (first_char < 'a' or first_char > 'z') return false; + + // Check if it contains a dot and ends with semicolon (but not a function call) + if (std.mem.indexOf(u8, after_eq, ".") == null) return false; + if (std.mem.indexOf(u8, after_eq, "(") != null) return false; + if (!std.mem.endsWith(u8, std.mem.trimRight(u8, after_eq, " \t"), ";")) return false; + + return true; +} + +fn getNewZigFiles(allocator: Allocator) !PathList { + var result = PathList{}; + errdefer { + for (result.items) |path| { + allocator.free(path); + } + result.deinit(allocator); + } + + // Run git diff to get new files + var child = std.process.Child.init(&.{ "git", "diff", "--name-only", "--diff-filter=A", "origin/main", "HEAD", "--", "src/" }, allocator); + child.stdout_behavior = .Pipe; + child.stderr_behavior = .Ignore; + + _ = child.spawn() catch { + // Git not available or not in a repo - return empty list + return result; + }; + + const stdout = child.stdout orelse return result; + const output = stdout.readToEndAlloc(allocator, max_file_bytes) catch return result; + defer allocator.free(output); + + const term = child.wait() catch return result; + if (term.Exited != 0) return result; + + // Parse output line by line + var lines = std.mem.splitScalar(u8, output, '\n'); + while (lines.next()) |line| { + if (line.len == 0) continue; + if (!std.mem.endsWith(u8, line, ".zig")) continue; + + try result.append(allocator, try allocator.dupe(u8, line)); + } + + return result; +} + +fn fileHasTopLevelComment(allocator: Allocator, file_path: []const u8) !bool { + const source = readSourceFile(allocator, file_path) catch |err| { + if (err == error.FileNotFound) { + // File was deleted but still shows in git diff - skip it + return true; + } + return err; + }; + defer allocator.free(source); + + return std.mem.indexOf(u8, source, "//!") != null; +} + +fn readSourceFile(allocator: Allocator, path: []const u8) ![:0]u8 { + return try std.fs.cwd().readFileAllocOptions( + allocator, + path, + max_file_bytes, + null, + std.mem.Alignment.of(u8), + 0, + ); +} + +fn freePathList(list: *PathList, allocator: Allocator) void { + for (list.items) |path| { + allocator.free(path); + } + list.deinit(allocator); +} diff --git a/crates/cli/tests/cli_tests.rs b/crates/cli/tests/cli_tests.rs index 69c47c8e84..12e1486a36 100644 --- a/crates/cli/tests/cli_tests.rs +++ b/crates/cli/tests/cli_tests.rs @@ -1111,6 +1111,20 @@ mod cli_tests { let cli_dev_out = cli_dev.run(); cli_dev_out.assert_clean_success(); } + + #[test] + #[cfg_attr(windows, ignore)] + fn effectful_join_map() { + build_platform_host(); + + let cli_dev = ExecCli::new( + roc_cli::CMD_DEV, + file_from_root("crates/cli/tests/test-projects/effectful", "join_map.roc"), + ); + + let cli_dev_out = cli_dev.run(); + cli_dev_out.assert_clean_success(); + } } // this is for testing the benchmarks (on small inputs), to perform proper benchmarks see crates/cli/benches/README.md diff --git a/crates/cli/tests/test-projects/effectful/join_map.roc b/crates/cli/tests/test-projects/effectful/join_map.roc new file mode 100644 index 0000000000..ab78e2cc44 --- /dev/null +++ b/crates/cli/tests/test-projects/effectful/join_map.roc @@ -0,0 +1,21 @@ +app [main!] { pf: platform "../test-platform-effects-zig/main.roc" } + +import pf.Effect + +main! : {} => {} +main! = \{} -> + flattened = List.join_map!([1, 2, 3], duplicate!) + expect flattened == Ok([1, 1, 2, 2, 3, 3]) + + with_zero = List.join_map!([0, 2], duplicate!) + expect with_zero == Ok([0, 0, 2, 2]) + + empty = List.join_map!([], duplicate!) + expect empty == Ok([]) + + {} + +duplicate! : U64 => Result (List U64) _ +duplicate! = \num -> + value = Effect.id_effectful!(num) + Ok([value, value]) diff --git a/crates/compiler/builtins/roc/Decode.roc b/crates/compiler/builtins/roc/Decode.roc index 096005f2aa..59d7aee7bd 100644 --- a/crates/compiler/builtins/roc/Decode.roc +++ b/crates/compiler/builtins/roc/Decode.roc @@ -60,7 +60,7 @@ DecodeError : [TooShort] ## ```roc ## expect ## input = "\"hello\", " |> Str.to_utf8 -## actual = Decode.from_bytes_partial(input, Json.json) +## actual = Decode.from_bytes_partial(input, Json.utf8) ## expected = Ok("hello") ## ## actual.result == expected @@ -133,7 +133,7 @@ decode_with = |bytes, @Decoder(decode), fmt| decode(bytes, fmt) ## ```roc ## expect ## input = "\"hello\", " |> Str.to_utf8 -## actual = Decode.from_bytes_partial(input Json.json) +## actual = Decode.from_bytes_partial(input Json.utf8) ## expected = Ok("hello") ## ## actual.result == expected @@ -147,7 +147,7 @@ from_bytes_partial = |bytes, fmt| decode_with(bytes, decoder, fmt) ## ```roc ## expect ## input = "\"hello\", " |> Str.to_utf8 -## actual = Decode.from_bytes(input, Json.json) +## actual = Decode.from_bytes(input, Json.utf8) ## expected = Ok("hello") ## ## actual == expected diff --git a/crates/compiler/builtins/roc/Encode.roc b/crates/compiler/builtins/roc/Encode.roc index c3f4bfcb21..9e3a29b5ed 100644 --- a/crates/compiler/builtins/roc/Encode.roc +++ b/crates/compiler/builtins/roc/Encode.roc @@ -78,7 +78,7 @@ EncoderFormatting implements ## # Appends the byte 42 ## custom_encoder = Encode.custom(\bytes, _fmt -> List.append(bytes, 42)) ## -## actual = Encode.append_with([], custom_encoder, Core.json) +## actual = Encode.append_with([], custom_encoder, Json.utf8) ## expected = [42] # Expected result is a list with a single byte, 42 ## ## actual == expected @@ -93,7 +93,7 @@ append_with = |lst, @Encoder(do_encoding), fmt| do_encoding(lst, fmt) ## ## ```roc ## expect -## actual = Encode.append([], { foo: 43 }, Core.json) +## actual = Encode.append([], { foo: 43 }, Json.utf8) ## expected = Str.to_utf8("""{"foo":43}""") ## ## actual == expected @@ -107,7 +107,7 @@ append = |lst, val, fmt| append_with(lst, to_encoder(val), fmt) ## expect ## foo_rec = { foo: 42 } ## -## actual = Encode.to_bytes(foo_rec, Core.json) +## actual = Encode.to_bytes(foo_rec, Json.utf8) ## expected = Str.to_utf8("""{"foo":42}""") ## ## actual == expected diff --git a/crates/compiler/builtins/roc/List.roc b/crates/compiler/builtins/roc/List.roc index 2a84a9a8af..f5bd847560 100644 --- a/crates/compiler/builtins/roc/List.roc +++ b/crates/compiler/builtins/roc/List.roc @@ -38,6 +38,7 @@ module [ map3, map4, join_map, + join_map!, product, walk_with_index, walk_until, @@ -1156,6 +1157,26 @@ join_map : List a, (a -> List b) -> List b join_map = |list, mapper| List.walk(list, [], |state, elem| List.concat(state, mapper(elem))) +## Like [List.join_map], except the transformation function can have effects. +## +## ``` +## log_and_split! : List Str => Result (List Str) _ +## log_and_split! = |paths| +## List.join_map!( +## paths, +## |path| +## Result.map_ok(Stdout.line!(path), |_| Str.split_on(path, "/")), +## ) +## ``` +join_map! : List a, (a => Result (List b) err) => Result (List b) err +join_map! = |list, mapper!| + List.walk_try!( + list, + [], + |state, elem| + Result.map_ok(mapper!(elem), |mapper_list| List.concat(state, mapper_list)), + ) + ## Returns the first element of the list satisfying a predicate function. ## If no satisfying element is found, an `Err NotFound` is returned. find_first : List elem, (elem -> Bool) -> Result elem [NotFound] diff --git a/crates/compiler/builtins/roc/Str.roc b/crates/compiler/builtins/roc/Str.roc index 4968fb6cf2..095bddcfcd 100644 --- a/crates/compiler/builtins/roc/Str.roc +++ b/crates/compiler/builtins/roc/Str.roc @@ -49,6 +49,16 @@ ## ## Interpolation can be used in multiline strings, but the part inside the parentheses must still be on one line. ## +## ### Import Str from File +## +## To avoid verbose code to read the contents of a file into a Str, you can import it directly: +## +## ``` +## import "some-file.txt" as some_str : Str +## ``` +## +## Note: The file content is included in the Roc app executable, if you publish the executable, you do not need to provide the file alongside it. +## ## ### Escapes ## ## There are a few special escape sequences in strings: diff --git a/crates/compiler/checkmate/www/package-lock.json b/crates/compiler/checkmate/www/package-lock.json index 2a5f2b2b36..c14ab85749 100644 --- a/crates/compiler/checkmate/www/package-lock.json +++ b/crates/compiler/checkmate/www/package-lock.json @@ -91,6 +91,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.9.tgz", "integrity": "sha512-G2EgeufBcYw27U4hhoIwFcgc1XU7TlXJ3mv04oOv1WCuo900U/anZSPzEqNjwdjgffkk2Gs0AN0dW1CKVLcG7w==", "dev": true, + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.22.5", @@ -812,6 +813,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.22.5.tgz", "integrity": "sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ==", "dev": true, + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.22.5" }, @@ -1677,6 +1679,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.22.5.tgz", "integrity": "sha512-rog5gZaVbUip5iWDMTYbVM15XQq+RkUKhET/IHR6oizR+JEoN6CAfTTuHcK4vwUyzca30qqHqEpzBOnaRMWYMA==", "dev": true, + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.22.5", "@babel/helper-module-imports": "^7.22.5", @@ -2201,10 +2204,11 @@ "dev": true }, "node_modules/@bcherny/json-schema-ref-parser/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2573,10 +2577,11 @@ } }, "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -4691,6 +4696,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -4744,6 +4750,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -5097,6 +5104,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5192,6 +5200,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6041,6 +6050,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001646", "electron-to-chromium": "^1.5.4", @@ -6785,6 +6795,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -7148,6 +7159,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "peer": true, "engines": { "node": ">=12" } @@ -7944,6 +7956,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.45.0.tgz", "integrity": "sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", @@ -8359,6 +8372,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -8537,10 +8551,11 @@ } }, "node_modules/eslint/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -9490,6 +9505,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -10093,6 +10109,7 @@ "resolved": "https://registry.npmjs.org/immer/-/immer-9.0.21.tgz", "integrity": "sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA==", "devOptional": true, + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/immer" @@ -10780,6 +10797,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "dev": true, + "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -12816,10 +12834,11 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -13406,6 +13425,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -13585,10 +13605,11 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", + "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } @@ -14249,6 +14270,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", @@ -15383,6 +15405,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", "dev": true, + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -15756,6 +15779,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -15910,6 +15934,7 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" @@ -15935,6 +15960,7 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "dev": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -15957,6 +15983,7 @@ "version": "6.14.2", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.2.tgz", "integrity": "sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==", + "peer": true, "dependencies": { "@remix-run/router": "1.7.2", "react-router": "6.14.2" @@ -16417,6 +16444,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "dev": true, + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -17937,6 +17965,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, + "peer": true, "engines": { "node": ">=10" }, @@ -18036,6 +18065,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -18337,6 +18367,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, + "peer": true, "dependencies": { "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", @@ -18406,6 +18437,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -18459,6 +18491,7 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", "dev": true, + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -18518,6 +18551,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -18866,6 +18900,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", diff --git a/crates/compiler/fmt/src/migrate.rs b/crates/compiler/fmt/src/migrate.rs index b0f9ee161c..2ddf4918a5 100644 --- a/crates/compiler/fmt/src/migrate.rs +++ b/crates/compiler/fmt/src/migrate.rs @@ -864,6 +864,7 @@ fn is_static_method(module_name: &str, ident: &str) -> bool { | "map_try" | "walk_try" | "join_map" + | "join_map!" | "any" | "take_first" | "take_last" diff --git a/crates/compiler/module/src/symbol.rs b/crates/compiler/module/src/symbol.rs index 4c33c9e65e..54ac2f8060 100644 --- a/crates/compiler/module/src/symbol.rs +++ b/crates/compiler/module/src/symbol.rs @@ -1533,6 +1533,7 @@ define_builtins! { 95 LIST_WALK_TRY_FX: "walk_try!" 96 LIST_MAP_TRY_FX: "map_try!" 97 LIST_KEEP_IF_TRY_FX: "keep_if_try!" + 98 LIST_JOIN_MAP_FX: "join_map!" } 7 RESULT: "Result" => { 0 RESULT_RESULT: "Result" exposed_type=true // the Result.Result type alias diff --git a/crates/compiler/test_mono/generated/anonymous_closure_in_polymorphic_expression_issue_4717.txt b/crates/compiler/test_mono/generated/anonymous_closure_in_polymorphic_expression_issue_4717.txt index 72644a2988..c53d7877c9 100644 --- a/crates/compiler/test_mono/generated/anonymous_closure_in_polymorphic_expression_issue_4717.txt +++ b/crates/compiler/test_mono/generated/anonymous_closure_in_polymorphic_expression_issue_4717.txt @@ -2,81 +2,81 @@ procedure Bool.9 (#Attr.2, #Attr.3): let Bool.21 : Int1 = lowlevel Eq #Attr.2 #Attr.3; ret Bool.21; -procedure List.119 (List.582, List.583, List.584): - let List.712 : U64 = 0i64; - let List.713 : U64 = CallByName List.6 List.582; - let List.711 : [C U64, C U64] = CallByName List.80 List.582 List.583 List.584 List.712 List.713; - ret List.711; +procedure List.120 (List.590, List.591, List.592): + let List.720 : U64 = 0i64; + let List.721 : U64 = CallByName List.6 List.590; + let List.719 : [C U64, C U64] = CallByName List.80 List.590 List.591 List.592 List.720 List.721; + ret List.719; -procedure List.26 (List.216, List.217, List.218): - let List.705 : [C U64, C U64] = CallByName List.119 List.216 List.217 List.218; - let List.708 : U8 = 1i64; - let List.709 : U8 = GetTagId List.705; - let List.710 : Int1 = lowlevel Eq List.708 List.709; - if List.710 then - let List.219 : U64 = UnionAtIndex (Id 1) (Index 0) List.705; - ret List.219; - else - let List.220 : U64 = UnionAtIndex (Id 0) (Index 0) List.705; +procedure List.26 (List.217, List.218, List.219): + let List.713 : [C U64, C U64] = CallByName List.120 List.217 List.218 List.219; + let List.716 : U8 = 1i64; + let List.717 : U8 = GetTagId List.713; + let List.718 : Int1 = lowlevel Eq List.716 List.717; + if List.718 then + let List.220 : U64 = UnionAtIndex (Id 1) (Index 0) List.713; ret List.220; + else + let List.221 : U64 = UnionAtIndex (Id 0) (Index 0) List.713; + ret List.221; -procedure List.38 (List.413, List.414): - let List.704 : U64 = CallByName List.6 List.413; - let List.415 : U64 = CallByName Num.77 List.704 List.414; - let List.694 : List U8 = CallByName List.43 List.413 List.415; - ret List.694; +procedure List.38 (List.414, List.415): + let List.712 : U64 = CallByName List.6 List.414; + let List.416 : U64 = CallByName Num.77 List.712 List.415; + let List.702 : List U8 = CallByName List.43 List.414 List.416; + ret List.702; -procedure List.43 (List.411, List.412): - let List.702 : U64 = CallByName List.6 List.411; - let List.701 : U64 = CallByName Num.77 List.702 List.412; - let List.696 : {U64, U64} = Struct {List.412, List.701}; - let List.695 : List U8 = CallByName List.49 List.411 List.696; - ret List.695; - -procedure List.49 (List.489, List.490): - let List.698 : U64 = StructAtIndex 1 List.490; - let List.699 : U64 = StructAtIndex 0 List.490; - let List.697 : List U8 = CallByName List.72 List.489 List.698 List.699; - ret List.697; - -procedure List.6 (#Attr.2): - let List.703 : U64 = lowlevel ListLenU64 #Attr.2; +procedure List.43 (List.412, List.413): + let List.710 : U64 = CallByName List.6 List.412; + let List.709 : U64 = CallByName Num.77 List.710 List.413; + let List.704 : {U64, U64} = Struct {List.413, List.709}; + let List.703 : List U8 = CallByName List.49 List.412 List.704; ret List.703; +procedure List.49 (List.497, List.498): + let List.706 : U64 = StructAtIndex 1 List.498; + let List.707 : U64 = StructAtIndex 0 List.498; + let List.705 : List U8 = CallByName List.72 List.497 List.706 List.707; + ret List.705; + +procedure List.6 (#Attr.2): + let List.711 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.711; + procedure List.66 (#Attr.2, #Attr.3): - let List.726 : U8 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.726; + let List.734 : U8 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.734; procedure List.72 (#Attr.2, #Attr.3, #Attr.4): - let List.700 : List U8 = lowlevel ListSublist #Attr.2 #Attr.3 #Attr.4; - ret List.700; + let List.708 : List U8 = lowlevel ListSublist #Attr.2 #Attr.3 #Attr.4; + ret List.708; procedure List.80 (Bool.22, Bool.23, Bool.24, Bool.25, Bool.26): - joinpoint List.714 List.585 List.586 List.587 List.588 List.589: - let List.716 : Int1 = CallByName Num.22 List.588 List.589; - if List.716 then - let List.725 : U8 = CallByName List.66 List.585 List.588; - let List.717 : [C U64, C U64] = CallByName Test.4 List.586 List.725; - let List.722 : U8 = 1i64; - let List.723 : U8 = GetTagId List.717; - let List.724 : Int1 = lowlevel Eq List.722 List.723; - if List.724 then - let List.590 : U64 = UnionAtIndex (Id 1) (Index 0) List.717; - let List.720 : U64 = 1i64; - let List.719 : U64 = CallByName Num.51 List.588 List.720; - jump List.714 List.585 List.590 List.587 List.719 List.589; + joinpoint List.722 List.593 List.594 List.595 List.596 List.597: + let List.724 : Int1 = CallByName Num.22 List.596 List.597; + if List.724 then + let List.733 : U8 = CallByName List.66 List.593 List.596; + let List.725 : [C U64, C U64] = CallByName Test.4 List.594 List.733; + let List.730 : U8 = 1i64; + let List.731 : U8 = GetTagId List.725; + let List.732 : Int1 = lowlevel Eq List.730 List.731; + if List.732 then + let List.598 : U64 = UnionAtIndex (Id 1) (Index 0) List.725; + let List.728 : U64 = 1i64; + let List.727 : U64 = CallByName Num.51 List.596 List.728; + jump List.722 List.593 List.598 List.595 List.727 List.597; else - dec List.585; - let List.591 : U64 = UnionAtIndex (Id 0) (Index 0) List.717; - let List.721 : [C U64, C U64] = TagId(0) List.591; - ret List.721; + dec List.593; + let List.599 : U64 = UnionAtIndex (Id 0) (Index 0) List.725; + let List.729 : [C U64, C U64] = TagId(0) List.599; + ret List.729; else - dec List.585; - let List.715 : [C U64, C U64] = TagId(1) List.586; - ret List.715; + dec List.593; + let List.723 : [C U64, C U64] = TagId(1) List.594; + ret List.723; in inc Bool.22; - jump List.714 Bool.22 Bool.23 Bool.24 Bool.25 Bool.26; + jump List.722 Bool.22 Bool.23 Bool.24 Bool.25 Bool.26; procedure Num.22 (#Attr.2, #Attr.3): let Num.292 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/call_function_in_empty_list.txt b/crates/compiler/test_mono/generated/call_function_in_empty_list.txt index 36f04d4da8..f96f4db0c0 100644 --- a/crates/compiler/test_mono/generated/call_function_in_empty_list.txt +++ b/crates/compiler/test_mono/generated/call_function_in_empty_list.txt @@ -1,51 +1,51 @@ -procedure List.103 (Bool.21, Bool.22, Bool.23, Bool.24, Bool.25): - joinpoint List.700 List.178 List.179 List.180 List.181 List.182: - let List.702 : Int1 = CallByName Num.22 List.181 List.182; - if List.702 then - let List.706 : [] = CallByName List.66 List.178 List.181; - let List.183 : List {} = CallByName List.296 List.179 List.706 List.180; - let List.705 : U64 = 1i64; - let List.704 : U64 = CallByName Num.51 List.181 List.705; - jump List.700 List.178 List.183 List.180 List.704 List.182; +procedure List.104 (Bool.21, Bool.22, Bool.23, Bool.24, Bool.25): + joinpoint List.708 List.179 List.180 List.181 List.182 List.183: + let List.710 : Int1 = CallByName Num.22 List.182 List.183; + if List.710 then + let List.714 : [] = CallByName List.66 List.179 List.182; + let List.184 : List {} = CallByName List.297 List.180 List.714 List.181; + let List.713 : U64 = 1i64; + let List.712 : U64 = CallByName Num.51 List.182 List.713; + jump List.708 List.179 List.184 List.181 List.712 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc Bool.21; - jump List.700 Bool.21 Bool.22 Bool.23 Bool.24 Bool.25; + jump List.708 Bool.21 Bool.22 Bool.23 Bool.24 Bool.25; -procedure List.18 (List.175, List.176, List.177): - let List.698 : U64 = 0i64; - let List.699 : U64 = CallByName List.6 List.175; - let List.697 : List {} = CallByName List.103 List.175 List.176 List.177 List.698 List.699; - ret List.697; +procedure List.18 (List.176, List.177, List.178): + let List.706 : U64 = 0i64; + let List.707 : U64 = CallByName List.6 List.176; + let List.705 : List {} = CallByName List.104 List.176 List.177 List.178 List.706 List.707; + ret List.705; -procedure List.296 (List.297, List.298, List.294): - let List.711 : {} = CallByName Test.2 List.298; - let List.710 : List {} = CallByName List.71 List.297 List.711; - ret List.710; +procedure List.297 (List.298, List.299, List.295): + let List.719 : {} = CallByName Test.2 List.299; + let List.718 : List {} = CallByName List.71 List.298 List.719; + ret List.718; -procedure List.5 (List.293, List.294): - let List.295 : U64 = CallByName List.6 List.293; - let List.695 : List {} = CallByName List.68 List.295; - let List.694 : List {} = CallByName List.18 List.293 List.695 List.294; - ret List.694; +procedure List.5 (List.294, List.295): + let List.296 : U64 = CallByName List.6 List.294; + let List.703 : List {} = CallByName List.68 List.296; + let List.702 : List {} = CallByName List.18 List.294 List.703 List.295; + ret List.702; procedure List.6 (#Attr.2): - let List.708 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.708; + let List.716 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.716; procedure List.66 (#Attr.2, #Attr.3): - let List.707 : [] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.707; + let List.715 : [] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.715; procedure List.68 (#Attr.2): - let List.713 : List {} = lowlevel ListWithCapacity #Attr.2; - ret List.713; + let List.721 : List {} = lowlevel ListWithCapacity #Attr.2; + ret List.721; procedure List.71 (#Attr.2, #Attr.3): - let List.712 : List {} = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.712; + let List.720 : List {} = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.720; procedure Num.22 (#Attr.2, #Attr.3): let Num.290 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/call_function_in_empty_list_unbound.txt b/crates/compiler/test_mono/generated/call_function_in_empty_list_unbound.txt index f2451651f0..7064d8d72d 100644 --- a/crates/compiler/test_mono/generated/call_function_in_empty_list_unbound.txt +++ b/crates/compiler/test_mono/generated/call_function_in_empty_list_unbound.txt @@ -1,51 +1,51 @@ -procedure List.103 (Bool.21, Bool.22, Bool.23, Bool.24, Bool.25): - joinpoint List.700 List.178 List.179 List.180 List.181 List.182: - let List.702 : Int1 = CallByName Num.22 List.181 List.182; - if List.702 then - let List.706 : [] = CallByName List.66 List.178 List.181; - let List.183 : List [] = CallByName List.296 List.179 List.706 List.180; - let List.705 : U64 = 1i64; - let List.704 : U64 = CallByName Num.51 List.181 List.705; - jump List.700 List.178 List.183 List.180 List.704 List.182; +procedure List.104 (Bool.21, Bool.22, Bool.23, Bool.24, Bool.25): + joinpoint List.708 List.179 List.180 List.181 List.182 List.183: + let List.710 : Int1 = CallByName Num.22 List.182 List.183; + if List.710 then + let List.714 : [] = CallByName List.66 List.179 List.182; + let List.184 : List [] = CallByName List.297 List.180 List.714 List.181; + let List.713 : U64 = 1i64; + let List.712 : U64 = CallByName Num.51 List.182 List.713; + jump List.708 List.179 List.184 List.181 List.712 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc Bool.21; - jump List.700 Bool.21 Bool.22 Bool.23 Bool.24 Bool.25; + jump List.708 Bool.21 Bool.22 Bool.23 Bool.24 Bool.25; -procedure List.18 (List.175, List.176, List.177): - let List.698 : U64 = 0i64; - let List.699 : U64 = CallByName List.6 List.175; - let List.697 : List [] = CallByName List.103 List.175 List.176 List.177 List.698 List.699; - ret List.697; +procedure List.18 (List.176, List.177, List.178): + let List.706 : U64 = 0i64; + let List.707 : U64 = CallByName List.6 List.176; + let List.705 : List [] = CallByName List.104 List.176 List.177 List.178 List.706 List.707; + ret List.705; -procedure List.296 (List.297, List.298, List.294): - let List.711 : [] = CallByName Test.2 List.298; - let List.710 : List [] = CallByName List.71 List.297 List.711; - ret List.710; +procedure List.297 (List.298, List.299, List.295): + let List.719 : [] = CallByName Test.2 List.299; + let List.718 : List [] = CallByName List.71 List.298 List.719; + ret List.718; -procedure List.5 (List.293, List.294): - let List.295 : U64 = CallByName List.6 List.293; - let List.695 : List [] = CallByName List.68 List.295; - let List.694 : List [] = CallByName List.18 List.293 List.695 List.294; - ret List.694; +procedure List.5 (List.294, List.295): + let List.296 : U64 = CallByName List.6 List.294; + let List.703 : List [] = CallByName List.68 List.296; + let List.702 : List [] = CallByName List.18 List.294 List.703 List.295; + ret List.702; procedure List.6 (#Attr.2): - let List.708 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.708; + let List.716 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.716; procedure List.66 (#Attr.2, #Attr.3): - let List.707 : [] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.707; + let List.715 : [] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.715; procedure List.68 (#Attr.2): - let List.713 : List [] = lowlevel ListWithCapacity #Attr.2; - ret List.713; + let List.721 : List [] = lowlevel ListWithCapacity #Attr.2; + ret List.721; procedure List.71 (#Attr.2, #Attr.3): - let List.712 : List [] = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.712; + let List.720 : List [] = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.720; procedure Num.22 (#Attr.2, #Attr.3): let Num.290 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/capture_void_layout_task.txt b/crates/compiler/test_mono/generated/capture_void_layout_task.txt index 09be16d894..d819b5a6d2 100644 --- a/crates/compiler/test_mono/generated/capture_void_layout_task.txt +++ b/crates/compiler/test_mono/generated/capture_void_layout_task.txt @@ -1,32 +1,32 @@ -procedure List.103 (Bool.34, Bool.35, Bool.36, Bool.37, Bool.38): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : [] = CallByName List.66 List.178 List.181; - let List.183 : [C {}, C *self {{}, []}] = CallByName Test.29 List.179 List.703 List.180; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (Bool.34, Bool.35, Bool.36, Bool.37, Bool.38): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : [] = CallByName List.66 List.179 List.182; + let List.184 : [C {}, C *self {{}, []}] = CallByName Test.29 List.180 List.711 List.181; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc Bool.34; - jump List.697 Bool.34 Bool.35 Bool.36 Bool.37 Bool.38; + jump List.705 Bool.34 Bool.35 Bool.36 Bool.37 Bool.38; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : [C {}, C *self {{}, []}] = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : [C {}, C *self {{}, []}] = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; procedure List.6 (#Attr.2): - let List.705 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.705; + let List.713 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.713; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : [] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : [] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure Num.22 (#Attr.2, #Attr.3): let Num.290 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/closure_in_list.txt b/crates/compiler/test_mono/generated/closure_in_list.txt index 2dd9fb6483..2d88334764 100644 --- a/crates/compiler/test_mono/generated/closure_in_list.txt +++ b/crates/compiler/test_mono/generated/closure_in_list.txt @@ -1,6 +1,6 @@ procedure List.6 (#Attr.2): - let List.694 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.694; + let List.702 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.702; procedure Test.1 (Test.5): let Test.2 : I64 = 41i64; diff --git a/crates/compiler/test_mono/generated/compose_recursive_lambda_set_productive_nullable_wrapped.txt b/crates/compiler/test_mono/generated/compose_recursive_lambda_set_productive_nullable_wrapped.txt index 3673ec141c..686ded87a0 100644 --- a/crates/compiler/test_mono/generated/compose_recursive_lambda_set_productive_nullable_wrapped.txt +++ b/crates/compiler/test_mono/generated/compose_recursive_lambda_set_productive_nullable_wrapped.txt @@ -2,35 +2,35 @@ procedure Bool.2 (): let Bool.21 : Int1 = true; ret Bool.21; -procedure List.103 (Bool.29, Bool.30, Bool.31, Bool.32, Bool.33): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : Int1 = CallByName List.66 List.178 List.181; - let List.183 : [, C *self Int1, C *self Int1] = CallByName Test.6 List.179 List.703 List.180; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (Bool.29, Bool.30, Bool.31, Bool.32, Bool.33): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : Int1 = CallByName List.66 List.179 List.182; + let List.184 : [, C *self Int1, C *self Int1] = CallByName Test.6 List.180 List.711 List.181; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc Bool.29; - jump List.697 Bool.29 Bool.30 Bool.31 Bool.32 Bool.33; + jump List.705 Bool.29 Bool.30 Bool.31 Bool.32 Bool.33; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : [, C *self Int1, C *self Int1] = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : [, C *self Int1, C *self Int1] = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; procedure List.6 (#Attr.2): - let List.705 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.705; + let List.713 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.713; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : Int1 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : Int1 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure Num.22 (#Attr.2, #Attr.3): let Num.290 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/dict.txt b/crates/compiler/test_mono/generated/dict.txt index d9b1bea081..31daa071bd 100644 --- a/crates/compiler/test_mono/generated/dict.txt +++ b/crates/compiler/test_mono/generated/dict.txt @@ -26,8 +26,8 @@ procedure Dict.52 (): ret Dict.744; procedure List.6 (#Attr.2): - let List.694 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.694; + let List.702 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.702; procedure Num.75 (#Attr.2, #Attr.3): let Num.289 : U8 = lowlevel NumSubWrap #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/empty_list_of_function_type.txt b/crates/compiler/test_mono/generated/empty_list_of_function_type.txt index 2757c9516c..9e35b78e4a 100644 --- a/crates/compiler/test_mono/generated/empty_list_of_function_type.txt +++ b/crates/compiler/test_mono/generated/empty_list_of_function_type.txt @@ -2,25 +2,25 @@ procedure Bool.1 (): let Bool.21 : Int1 = false; ret Bool.21; -procedure List.2 (List.123, List.124): - let List.700 : U64 = CallByName List.6 List.123; - let List.696 : Int1 = CallByName Num.22 List.124 List.700; - if List.696 then - let List.698 : {} = CallByName List.66 List.123 List.124; - let List.697 : [C {}, C {}] = TagId(1) List.698; - ret List.697; +procedure List.2 (List.124, List.125): + let List.708 : U64 = CallByName List.6 List.124; + let List.704 : Int1 = CallByName Num.22 List.125 List.708; + if List.704 then + let List.706 : {} = CallByName List.66 List.124 List.125; + let List.705 : [C {}, C {}] = TagId(1) List.706; + ret List.705; else - let List.695 : {} = Struct {}; - let List.694 : [C {}, C {}] = TagId(0) List.695; - ret List.694; + let List.703 : {} = Struct {}; + let List.702 : [C {}, C {}] = TagId(0) List.703; + ret List.702; procedure List.6 (#Attr.2): - let List.701 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.701; + let List.709 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.709; procedure List.66 (#Attr.2, #Attr.3): - let List.699 : {} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.699; + let List.707 : {} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.707; procedure Num.22 (#Attr.2, #Attr.3): let Num.289 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/encode.txt b/crates/compiler/test_mono/generated/encode.txt index 284b55136f..0e284055f8 100644 --- a/crates/compiler/test_mono/generated/encode.txt +++ b/crates/compiler/test_mono/generated/encode.txt @@ -1,16 +1,16 @@ -procedure List.4 (List.139, List.140): - let List.697 : U64 = 1i64; - let List.695 : List U8 = CallByName List.70 List.139 List.697; - let List.694 : List U8 = CallByName List.71 List.695 List.140; - ret List.694; +procedure List.4 (List.140, List.141): + let List.705 : U64 = 1i64; + let List.703 : List U8 = CallByName List.70 List.140 List.705; + let List.702 : List U8 = CallByName List.71 List.703 List.141; + ret List.702; procedure List.70 (#Attr.2, #Attr.3): - let List.698 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.698; + let List.706 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.706; procedure List.71 (#Attr.2, #Attr.3): - let List.696 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.696; + let List.704 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.704; procedure Test.23 (Test.24, Test.35, Test.22): let Test.37 : List U8 = CallByName List.4 Test.24 Test.22; diff --git a/crates/compiler/test_mono/generated/encode_derived_nested_record_string.txt b/crates/compiler/test_mono/generated/encode_derived_nested_record_string.txt index bf935eee9c..b8964a3495 100644 --- a/crates/compiler/test_mono/generated/encode_derived_nested_record_string.txt +++ b/crates/compiler/test_mono/generated/encode_derived_nested_record_string.txt @@ -67,85 +67,85 @@ procedure Encode.26 (Encode.107, Encode.108): let Encode.110 : List U8 = CallByName Encode.24 Encode.111 Encode.112 Encode.108; ret Encode.110; -procedure List.103 (#Derived_gen.35, #Derived_gen.36, #Derived_gen.37, #Derived_gen.38, #Derived_gen.39): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : {Str, Str} = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : List U8 = CallByName Test.71 List.179 List.703; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.35, #Derived_gen.36, #Derived_gen.37, #Derived_gen.38, #Derived_gen.39): + joinpoint List.731 List.179 List.180 List.181 List.182 List.183: + let List.733 : Int1 = CallByName Num.22 List.182 List.183; + if List.733 then + let List.737 : {Str, Str} = CallByName List.66 List.179 List.182; + inc List.737; + let List.184 : List U8 = CallByName Test.71 List.180 List.737; + let List.736 : U64 = 1i64; + let List.735 : U64 = CallByName Num.51 List.182 List.736; + jump List.731 List.179 List.184 List.181 List.735 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.35; - jump List.697 #Derived_gen.35 #Derived_gen.36 #Derived_gen.37 #Derived_gen.38 #Derived_gen.39; + jump List.731 #Derived_gen.35 #Derived_gen.36 #Derived_gen.37 #Derived_gen.38 #Derived_gen.39; -procedure List.103 (#Derived_gen.40, #Derived_gen.41, #Derived_gen.42, #Derived_gen.43, #Derived_gen.44): - joinpoint List.723 List.178 List.179 List.180 List.181 List.182: - let List.725 : Int1 = CallByName Num.22 List.181 List.182; - if List.725 then - let List.729 : {Str, Str} = CallByName List.66 List.178 List.181; - inc List.729; - let List.183 : List U8 = CallByName Test.71 List.179 List.729; - let List.728 : U64 = 1i64; - let List.727 : U64 = CallByName Num.51 List.181 List.728; - jump List.723 List.178 List.183 List.180 List.727 List.182; +procedure List.104 (#Derived_gen.40, #Derived_gen.41, #Derived_gen.42, #Derived_gen.43, #Derived_gen.44): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : {Str, Str} = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : List U8 = CallByName Test.71 List.180 List.711; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.40; - jump List.723 #Derived_gen.40 #Derived_gen.41 #Derived_gen.42 #Derived_gen.43 #Derived_gen.44; + jump List.705 #Derived_gen.40 #Derived_gen.41 #Derived_gen.42 #Derived_gen.43 #Derived_gen.44; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : List U8 = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : List U8 = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; -procedure List.18 (List.175, List.176, List.177): - let List.721 : U64 = 0i64; - let List.722 : U64 = CallByName List.6 List.175; - let List.720 : List U8 = CallByName List.103 List.175 List.176 List.177 List.721 List.722; - ret List.720; +procedure List.18 (List.176, List.177, List.178): + let List.729 : U64 = 0i64; + let List.730 : U64 = CallByName List.6 List.176; + let List.728 : List U8 = CallByName List.104 List.176 List.177 List.178 List.729 List.730; + ret List.728; -procedure List.4 (List.139, List.140): - let List.742 : U64 = 1i64; - let List.741 : List U8 = CallByName List.70 List.139 List.742; - let List.740 : List U8 = CallByName List.71 List.741 List.140; - ret List.740; +procedure List.4 (List.140, List.141): + let List.750 : U64 = 1i64; + let List.749 : List U8 = CallByName List.70 List.140 List.750; + let List.748 : List U8 = CallByName List.71 List.749 List.141; + ret List.748; procedure List.6 (#Attr.2): - let List.719 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.719; + let List.727 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.727; procedure List.6 (#Attr.2): - let List.745 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.745; + let List.753 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.753; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure List.66 (#Attr.2, #Attr.3): - let List.730 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.730; + let List.738 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.738; procedure List.70 (#Attr.2, #Attr.3): - let List.736 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.736; + let List.744 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.744; procedure List.71 (#Attr.2, #Attr.3): - let List.734 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.734; + let List.742 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.742; procedure List.8 (#Attr.2, #Attr.3): - let List.744 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.744; + let List.752 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.752; procedure Num.127 (#Attr.2): let Num.294 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/encode_derived_record_one_field_string.txt b/crates/compiler/test_mono/generated/encode_derived_record_one_field_string.txt index 73888e6277..df94a605c7 100644 --- a/crates/compiler/test_mono/generated/encode_derived_record_one_field_string.txt +++ b/crates/compiler/test_mono/generated/encode_derived_record_one_field_string.txt @@ -39,54 +39,54 @@ procedure Encode.26 (Encode.107, Encode.108): let Encode.110 : List U8 = CallByName Encode.24 Encode.111 Encode.112 Encode.108; ret Encode.110; -procedure List.103 (#Derived_gen.19, #Derived_gen.20, #Derived_gen.21, #Derived_gen.22, #Derived_gen.23): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : {Str, Str} = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : List U8 = CallByName Test.71 List.179 List.703; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.19, #Derived_gen.20, #Derived_gen.21, #Derived_gen.22, #Derived_gen.23): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : {Str, Str} = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : List U8 = CallByName Test.71 List.180 List.711; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.19; - jump List.697 #Derived_gen.19 #Derived_gen.20 #Derived_gen.21 #Derived_gen.22 #Derived_gen.23; + jump List.705 #Derived_gen.19 #Derived_gen.20 #Derived_gen.21 #Derived_gen.22 #Derived_gen.23; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : List U8 = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : List U8 = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; -procedure List.4 (List.139, List.140): - let List.716 : U64 = 1i64; - let List.715 : List U8 = CallByName List.70 List.139 List.716; - let List.714 : List U8 = CallByName List.71 List.715 List.140; - ret List.714; +procedure List.4 (List.140, List.141): + let List.724 : U64 = 1i64; + let List.723 : List U8 = CallByName List.70 List.140 List.724; + let List.722 : List U8 = CallByName List.71 List.723 List.141; + ret List.722; procedure List.6 (#Attr.2): - let List.719 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.719; + let List.727 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.727; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure List.70 (#Attr.2, #Attr.3): - let List.710 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.710; + let List.718 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.718; procedure List.71 (#Attr.2, #Attr.3): - let List.708 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.708; + let List.716 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.716; procedure List.8 (#Attr.2, #Attr.3): - let List.718 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.718; + let List.726 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.726; procedure Num.127 (#Attr.2): let Num.290 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/encode_derived_record_two_field_strings.txt b/crates/compiler/test_mono/generated/encode_derived_record_two_field_strings.txt index 7dbefe6d0b..f88f9400c8 100644 --- a/crates/compiler/test_mono/generated/encode_derived_record_two_field_strings.txt +++ b/crates/compiler/test_mono/generated/encode_derived_record_two_field_strings.txt @@ -46,54 +46,54 @@ procedure Encode.26 (Encode.107, Encode.108): let Encode.110 : List U8 = CallByName Encode.24 Encode.111 Encode.112 Encode.108; ret Encode.110; -procedure List.103 (#Derived_gen.23, #Derived_gen.24, #Derived_gen.25, #Derived_gen.26, #Derived_gen.27): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : {Str, Str} = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : List U8 = CallByName Test.71 List.179 List.703; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.23, #Derived_gen.24, #Derived_gen.25, #Derived_gen.26, #Derived_gen.27): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : {Str, Str} = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : List U8 = CallByName Test.71 List.180 List.711; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.23; - jump List.697 #Derived_gen.23 #Derived_gen.24 #Derived_gen.25 #Derived_gen.26 #Derived_gen.27; + jump List.705 #Derived_gen.23 #Derived_gen.24 #Derived_gen.25 #Derived_gen.26 #Derived_gen.27; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : List U8 = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : List U8 = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; -procedure List.4 (List.139, List.140): - let List.716 : U64 = 1i64; - let List.715 : List U8 = CallByName List.70 List.139 List.716; - let List.714 : List U8 = CallByName List.71 List.715 List.140; - ret List.714; +procedure List.4 (List.140, List.141): + let List.724 : U64 = 1i64; + let List.723 : List U8 = CallByName List.70 List.140 List.724; + let List.722 : List U8 = CallByName List.71 List.723 List.141; + ret List.722; procedure List.6 (#Attr.2): - let List.719 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.719; + let List.727 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.727; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure List.70 (#Attr.2, #Attr.3): - let List.710 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.710; + let List.718 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.718; procedure List.71 (#Attr.2, #Attr.3): - let List.708 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.708; + let List.716 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.716; procedure List.8 (#Attr.2, #Attr.3): - let List.718 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.718; + let List.726 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.726; procedure Num.127 (#Attr.2): let Num.290 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/encode_derived_string.txt b/crates/compiler/test_mono/generated/encode_derived_string.txt index d4162198e9..9570ef9d9d 100644 --- a/crates/compiler/test_mono/generated/encode_derived_string.txt +++ b/crates/compiler/test_mono/generated/encode_derived_string.txt @@ -11,23 +11,23 @@ procedure Encode.26 (Encode.107, Encode.108): let Encode.110 : List U8 = CallByName Encode.24 Encode.111 Encode.112 Encode.108; ret Encode.110; -procedure List.4 (List.139, List.140): - let List.704 : U64 = 1i64; - let List.703 : List U8 = CallByName List.70 List.139 List.704; - let List.702 : List U8 = CallByName List.71 List.703 List.140; - ret List.702; +procedure List.4 (List.140, List.141): + let List.712 : U64 = 1i64; + let List.711 : List U8 = CallByName List.70 List.140 List.712; + let List.710 : List U8 = CallByName List.71 List.711 List.141; + ret List.710; procedure List.70 (#Attr.2, #Attr.3): - let List.698 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.698; + let List.706 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.706; procedure List.71 (#Attr.2, #Attr.3): - let List.696 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.696; + let List.704 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.704; procedure List.8 (#Attr.2, #Attr.3): - let List.706 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.706; + let List.714 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.714; procedure Num.127 (#Attr.2): let Num.290 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/encode_derived_tag_one_field_string.txt b/crates/compiler/test_mono/generated/encode_derived_tag_one_field_string.txt index 75987fbbf4..32cf323715 100644 --- a/crates/compiler/test_mono/generated/encode_derived_tag_one_field_string.txt +++ b/crates/compiler/test_mono/generated/encode_derived_tag_one_field_string.txt @@ -40,58 +40,58 @@ procedure Encode.26 (Encode.107, Encode.108): let Encode.110 : List U8 = CallByName Encode.24 Encode.111 Encode.112 Encode.108; ret Encode.110; -procedure List.103 (#Derived_gen.22, #Derived_gen.23, #Derived_gen.24, #Derived_gen.25, #Derived_gen.26): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : Str = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : List U8 = CallByName Test.64 List.179 List.703 List.180; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.22, #Derived_gen.23, #Derived_gen.24, #Derived_gen.25, #Derived_gen.26): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : Str = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : List U8 = CallByName Test.64 List.180 List.711 List.181; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.22; - jump List.697 #Derived_gen.22 #Derived_gen.23 #Derived_gen.24 #Derived_gen.25 #Derived_gen.26; + jump List.705 #Derived_gen.22 #Derived_gen.23 #Derived_gen.24 #Derived_gen.25 #Derived_gen.26; procedure List.13 (#Attr.2, #Attr.3): - let List.720 : List Str = lowlevel ListPrepend #Attr.2 #Attr.3; - ret List.720; + let List.728 : List Str = lowlevel ListPrepend #Attr.2 #Attr.3; + ret List.728; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : List U8 = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : List U8 = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; -procedure List.4 (List.139, List.140): - let List.716 : U64 = 1i64; - let List.715 : List U8 = CallByName List.70 List.139 List.716; - let List.714 : List U8 = CallByName List.71 List.715 List.140; - ret List.714; +procedure List.4 (List.140, List.141): + let List.724 : U64 = 1i64; + let List.723 : List U8 = CallByName List.70 List.140 List.724; + let List.722 : List U8 = CallByName List.71 List.723 List.141; + ret List.722; procedure List.6 (#Attr.2): - let List.719 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.719; + let List.727 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.727; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure List.70 (#Attr.2, #Attr.3): - let List.710 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.710; + let List.718 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.718; procedure List.71 (#Attr.2, #Attr.3): - let List.708 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.708; + let List.716 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.716; procedure List.8 (#Attr.2, #Attr.3): - let List.718 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.718; + let List.726 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.726; procedure Num.127 (#Attr.2): let Num.290 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/encode_derived_tag_two_payloads_string.txt b/crates/compiler/test_mono/generated/encode_derived_tag_two_payloads_string.txt index f6b4e474fa..4916026b25 100644 --- a/crates/compiler/test_mono/generated/encode_derived_tag_two_payloads_string.txt +++ b/crates/compiler/test_mono/generated/encode_derived_tag_two_payloads_string.txt @@ -43,58 +43,58 @@ procedure Encode.26 (Encode.107, Encode.108): let Encode.110 : List U8 = CallByName Encode.24 Encode.111 Encode.112 Encode.108; ret Encode.110; -procedure List.103 (#Derived_gen.23, #Derived_gen.24, #Derived_gen.25, #Derived_gen.26, #Derived_gen.27): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : Str = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : List U8 = CallByName Test.64 List.179 List.703 List.180; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.23, #Derived_gen.24, #Derived_gen.25, #Derived_gen.26, #Derived_gen.27): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : Str = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : List U8 = CallByName Test.64 List.180 List.711 List.181; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.23; - jump List.697 #Derived_gen.23 #Derived_gen.24 #Derived_gen.25 #Derived_gen.26 #Derived_gen.27; + jump List.705 #Derived_gen.23 #Derived_gen.24 #Derived_gen.25 #Derived_gen.26 #Derived_gen.27; procedure List.13 (#Attr.2, #Attr.3): - let List.720 : List Str = lowlevel ListPrepend #Attr.2 #Attr.3; - ret List.720; + let List.728 : List Str = lowlevel ListPrepend #Attr.2 #Attr.3; + ret List.728; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : List U8 = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : List U8 = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; -procedure List.4 (List.139, List.140): - let List.716 : U64 = 1i64; - let List.715 : List U8 = CallByName List.70 List.139 List.716; - let List.714 : List U8 = CallByName List.71 List.715 List.140; - ret List.714; +procedure List.4 (List.140, List.141): + let List.724 : U64 = 1i64; + let List.723 : List U8 = CallByName List.70 List.140 List.724; + let List.722 : List U8 = CallByName List.71 List.723 List.141; + ret List.722; procedure List.6 (#Attr.2): - let List.719 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.719; + let List.727 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.727; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure List.70 (#Attr.2, #Attr.3): - let List.710 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.710; + let List.718 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.718; procedure List.71 (#Attr.2, #Attr.3): - let List.708 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.708; + let List.716 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.716; procedure List.8 (#Attr.2, #Attr.3): - let List.718 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.718; + let List.726 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.726; procedure Num.127 (#Attr.2): let Num.290 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/inspect_derived_dict.txt b/crates/compiler/test_mono/generated/inspect_derived_dict.txt index d0ea73fe19..213f7f88ba 100644 --- a/crates/compiler/test_mono/generated/inspect_derived_dict.txt +++ b/crates/compiler/test_mono/generated/inspect_derived_dict.txt @@ -898,171 +898,171 @@ procedure Inspect.63 (Inspect.295, Inspect.291): procedure Inspect.64 (Inspect.297): ret Inspect.297; -procedure List.101 (#Derived_gen.34, #Derived_gen.35, #Derived_gen.36): - joinpoint List.745 List.155 List.156 List.157: - let List.753 : U64 = 0i64; - let List.747 : Int1 = CallByName Num.24 List.156 List.753; - if List.747 then - let List.752 : U64 = 1i64; - let List.749 : U64 = CallByName Num.75 List.156 List.752; - let List.750 : List {U32, U32} = CallByName List.71 List.157 List.155; - jump List.745 List.155 List.749 List.750; +procedure List.102 (#Derived_gen.34, #Derived_gen.35, #Derived_gen.36): + joinpoint List.753 List.156 List.157 List.158: + let List.761 : U64 = 0i64; + let List.755 : Int1 = CallByName Num.24 List.157 List.761; + if List.755 then + let List.760 : U64 = 1i64; + let List.757 : U64 = CallByName Num.75 List.157 List.760; + let List.758 : List {U32, U32} = CallByName List.71 List.158 List.156; + jump List.753 List.156 List.757 List.758; else - ret List.157; + ret List.158; in - jump List.745 #Derived_gen.34 #Derived_gen.35 #Derived_gen.36; + jump List.753 #Derived_gen.34 #Derived_gen.35 #Derived_gen.36; -procedure List.103 (#Derived_gen.37, #Derived_gen.38, #Derived_gen.39, #Derived_gen.40, #Derived_gen.41): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : {Str, I64} = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : {List {U32, U32}, List {Str, I64}, U64, Float32, U8} = CallByName Dict.159 List.179 List.703; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.37, #Derived_gen.38, #Derived_gen.39, #Derived_gen.40, #Derived_gen.41): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : {Str, I64} = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : {List {U32, U32}, List {Str, I64}, U64, Float32, U8} = CallByName Dict.159 List.180 List.711; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.37; - jump List.697 #Derived_gen.37 #Derived_gen.38 #Derived_gen.39 #Derived_gen.40 #Derived_gen.41; + jump List.705 #Derived_gen.37 #Derived_gen.38 #Derived_gen.39 #Derived_gen.40 #Derived_gen.41; -procedure List.103 (#Derived_gen.42, #Derived_gen.43, #Derived_gen.44, #Derived_gen.45, #Derived_gen.46): - joinpoint List.760 List.178 List.179 List.180 List.181 List.182: - let List.762 : Int1 = CallByName Num.22 List.181 List.182; - if List.762 then - let List.766 : {Str, I64} = CallByName List.66 List.178 List.181; - inc List.766; - let List.183 : {Str, Int1} = CallByName Dict.188 List.179 List.766 List.180; - let List.765 : U64 = 1i64; - let List.764 : U64 = CallByName Num.51 List.181 List.765; - jump List.760 List.178 List.183 List.180 List.764 List.182; +procedure List.104 (#Derived_gen.42, #Derived_gen.43, #Derived_gen.44, #Derived_gen.45, #Derived_gen.46): + joinpoint List.768 List.179 List.180 List.181 List.182 List.183: + let List.770 : Int1 = CallByName Num.22 List.182 List.183; + if List.770 then + let List.774 : {Str, I64} = CallByName List.66 List.179 List.182; + inc List.774; + let List.184 : {Str, Int1} = CallByName Dict.188 List.180 List.774 List.181; + let List.773 : U64 = 1i64; + let List.772 : U64 = CallByName Num.51 List.182 List.773; + jump List.768 List.179 List.184 List.181 List.772 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.42; - jump List.760 #Derived_gen.42 #Derived_gen.43 #Derived_gen.44 #Derived_gen.45 #Derived_gen.46; + jump List.768 #Derived_gen.42 #Derived_gen.43 #Derived_gen.44 #Derived_gen.45 #Derived_gen.46; -procedure List.104 (#Derived_gen.47, #Derived_gen.48, #Derived_gen.49, #Derived_gen.50, #Derived_gen.51): - joinpoint List.736 List.187 List.188 List.189 List.190 List.191: - let List.738 : Int1 = CallByName Num.22 List.190 List.191; - if List.738 then - let List.742 : {Str, I64} = CallByName List.66 List.187 List.190; - inc List.742; - let List.192 : List {U32, U32} = CallByName Dict.407 List.188 List.742 List.190 List.189; - let List.741 : U64 = 1i64; - let List.740 : U64 = CallByName Num.51 List.190 List.741; - jump List.736 List.187 List.192 List.189 List.740 List.191; +procedure List.105 (#Derived_gen.47, #Derived_gen.48, #Derived_gen.49, #Derived_gen.50, #Derived_gen.51): + joinpoint List.744 List.188 List.189 List.190 List.191 List.192: + let List.746 : Int1 = CallByName Num.22 List.191 List.192; + if List.746 then + let List.750 : {Str, I64} = CallByName List.66 List.188 List.191; + inc List.750; + let List.193 : List {U32, U32} = CallByName Dict.407 List.189 List.750 List.191 List.190; + let List.749 : U64 = 1i64; + let List.748 : U64 = CallByName Num.51 List.191 List.749; + jump List.744 List.188 List.193 List.190 List.748 List.192; else - dec List.187; - ret List.188; + dec List.188; + ret List.189; in inc #Derived_gen.47; - jump List.736 #Derived_gen.47 #Derived_gen.48 #Derived_gen.49 #Derived_gen.50 #Derived_gen.51; + jump List.744 #Derived_gen.47 #Derived_gen.48 #Derived_gen.49 #Derived_gen.50 #Derived_gen.51; -procedure List.11 (List.153, List.154): - let List.756 : List {U32, U32} = CallByName List.68 List.154; - let List.755 : List {U32, U32} = CallByName List.101 List.153 List.154 List.756; - ret List.755; +procedure List.11 (List.154, List.155): + let List.764 : List {U32, U32} = CallByName List.68 List.155; + let List.763 : List {U32, U32} = CallByName List.102 List.154 List.155 List.764; + ret List.763; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : {List {U32, U32}, List {Str, I64}, U64, Float32, U8} = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : {List {U32, U32}, List {Str, I64}, U64, Float32, U8} = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; -procedure List.18 (List.175, List.176, List.177): - let List.758 : U64 = 0i64; - let List.759 : U64 = CallByName List.6 List.175; - let List.757 : {Str, Int1} = CallByName List.103 List.175 List.176 List.177 List.758 List.759; - ret List.757; +procedure List.18 (List.176, List.177, List.178): + let List.766 : U64 = 0i64; + let List.767 : U64 = CallByName List.6 List.176; + let List.765 : {Str, Int1} = CallByName List.104 List.176 List.177 List.178 List.766 List.767; + ret List.765; -procedure List.3 (List.131, List.132, List.133): - let List.720 : {List {U32, U32}, {U32, U32}} = CallByName List.64 List.131 List.132 List.133; - let List.719 : List {U32, U32} = StructAtIndex 0 List.720; - ret List.719; +procedure List.3 (List.132, List.133, List.134): + let List.728 : {List {U32, U32}, {U32, U32}} = CallByName List.64 List.132 List.133 List.134; + let List.727 : List {U32, U32} = StructAtIndex 0 List.728; + ret List.727; -procedure List.3 (List.131, List.132, List.133): - let List.722 : {List {Str, I64}, {Str, I64}} = CallByName List.64 List.131 List.132 List.133; - let List.721 : List {Str, I64} = StructAtIndex 0 List.722; - let #Derived_gen.74 : {Str, I64} = StructAtIndex 1 List.722; +procedure List.3 (List.132, List.133, List.134): + let List.730 : {List {Str, I64}, {Str, I64}} = CallByName List.64 List.132 List.133 List.134; + let List.729 : List {Str, I64} = StructAtIndex 0 List.730; + let #Derived_gen.74 : {Str, I64} = StructAtIndex 1 List.730; dec #Derived_gen.74; - ret List.721; + ret List.729; -procedure List.4 (List.139, List.140): - let List.731 : U64 = 1i64; - let List.729 : List {Str, I64} = CallByName List.70 List.139 List.731; - let List.728 : List {Str, I64} = CallByName List.71 List.729 List.140; - ret List.728; +procedure List.4 (List.140, List.141): + let List.739 : U64 = 1i64; + let List.737 : List {Str, I64} = CallByName List.70 List.140 List.739; + let List.736 : List {Str, I64} = CallByName List.71 List.737 List.141; + ret List.736; procedure List.6 (#Attr.2): - let List.710 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.710; + let List.718 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.718; procedure List.6 (#Attr.2): - let List.768 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.768; + let List.776 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.776; procedure List.6 (#Attr.2): - let List.769 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.769; + let List.777 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.777; -procedure List.64 (List.128, List.129, List.130): - let List.718 : U64 = CallByName List.6 List.128; - let List.715 : Int1 = CallByName Num.22 List.129 List.718; - if List.715 then - let List.716 : {List {U32, U32}, {U32, U32}} = CallByName List.67 List.128 List.129 List.130; - ret List.716; +procedure List.64 (List.129, List.130, List.131): + let List.726 : U64 = CallByName List.6 List.129; + let List.723 : Int1 = CallByName Num.22 List.130 List.726; + if List.723 then + let List.724 : {List {U32, U32}, {U32, U32}} = CallByName List.67 List.129 List.130 List.131; + ret List.724; else - let List.714 : {List {U32, U32}, {U32, U32}} = Struct {List.128, List.130}; - ret List.714; + let List.722 : {List {U32, U32}, {U32, U32}} = Struct {List.129, List.131}; + ret List.722; -procedure List.64 (List.128, List.129, List.130): - let List.727 : U64 = CallByName List.6 List.128; - let List.724 : Int1 = CallByName Num.22 List.129 List.727; - if List.724 then - let List.725 : {List {Str, I64}, {Str, I64}} = CallByName List.67 List.128 List.129 List.130; - ret List.725; +procedure List.64 (List.129, List.130, List.131): + let List.735 : U64 = CallByName List.6 List.129; + let List.732 : Int1 = CallByName Num.22 List.130 List.735; + if List.732 then + let List.733 : {List {Str, I64}, {Str, I64}} = CallByName List.67 List.129 List.130 List.131; + ret List.733; else - let List.723 : {List {Str, I64}, {Str, I64}} = Struct {List.128, List.130}; - ret List.723; + let List.731 : {List {Str, I64}, {Str, I64}} = Struct {List.129, List.131}; + ret List.731; procedure List.66 (#Attr.2, #Attr.3): - let List.767 : {Str, I64} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.767; + let List.775 : {Str, I64} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.775; procedure List.67 (#Attr.2, #Attr.3, #Attr.4): - let List.717 : {List {U32, U32}, {U32, U32}} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; - ret List.717; + let List.725 : {List {U32, U32}, {U32, U32}} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; + ret List.725; procedure List.67 (#Attr.2, #Attr.3, #Attr.4): - let List.726 : {List {Str, I64}, {Str, I64}} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; - ret List.726; + let List.734 : {List {Str, I64}, {Str, I64}} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; + ret List.734; procedure List.68 (#Attr.2): - let List.754 : List {U32, U32} = lowlevel ListWithCapacity #Attr.2; - ret List.754; + let List.762 : List {U32, U32} = lowlevel ListWithCapacity #Attr.2; + ret List.762; procedure List.70 (#Attr.2, #Attr.3): - let List.732 : List {Str, I64} = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.732; + let List.740 : List {Str, I64} = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.740; procedure List.71 (#Attr.2, #Attr.3): - let List.730 : List {Str, I64} = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.730; + let List.738 : List {Str, I64} = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.738; procedure List.71 (#Attr.2, #Attr.3): - let List.751 : List {U32, U32} = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.751; + let List.759 : List {U32, U32} = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.759; -procedure List.83 (List.184, List.185, List.186): - let List.734 : U64 = 0i64; - let List.735 : U64 = CallByName List.6 List.184; - let List.733 : List {U32, U32} = CallByName List.104 List.184 List.185 List.186 List.734 List.735; - ret List.733; +procedure List.83 (List.185, List.186, List.187): + let List.742 : U64 = 0i64; + let List.743 : U64 = CallByName List.6 List.185; + let List.741 : List {U32, U32} = CallByName List.105 List.185 List.186 List.187 List.742 List.743; + ret List.741; procedure Num.131 (#Attr.2): let Num.297 : U32 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/inspect_derived_list.txt b/crates/compiler/test_mono/generated/inspect_derived_list.txt index 084572e4f0..d2915b9db2 100644 --- a/crates/compiler/test_mono/generated/inspect_derived_list.txt +++ b/crates/compiler/test_mono/generated/inspect_derived_list.txt @@ -120,35 +120,35 @@ procedure Inspect.63 (Inspect.295, Inspect.291): procedure Inspect.64 (Inspect.297): ret Inspect.297; -procedure List.103 (#Derived_gen.10, #Derived_gen.11, #Derived_gen.12, #Derived_gen.13, #Derived_gen.14): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : I64 = CallByName List.66 List.178 List.181; - let List.183 : {Str, Int1} = CallByName Inspect.160 List.179 List.703 List.180; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.10, #Derived_gen.11, #Derived_gen.12, #Derived_gen.13, #Derived_gen.14): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : I64 = CallByName List.66 List.179 List.182; + let List.184 : {Str, Int1} = CallByName Inspect.160 List.180 List.711 List.181; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.10; - jump List.697 #Derived_gen.10 #Derived_gen.11 #Derived_gen.12 #Derived_gen.13 #Derived_gen.14; + jump List.705 #Derived_gen.10 #Derived_gen.11 #Derived_gen.12 #Derived_gen.13 #Derived_gen.14; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : {Str, Int1} = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : {Str, Int1} = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; procedure List.6 (#Attr.2): - let List.705 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.705; + let List.713 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.713; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure Num.22 (#Attr.2, #Attr.3): let Num.291 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/inspect_derived_nested_record_string.txt b/crates/compiler/test_mono/generated/inspect_derived_nested_record_string.txt index b1672b6eaf..0eedef01bc 100644 --- a/crates/compiler/test_mono/generated/inspect_derived_nested_record_string.txt +++ b/crates/compiler/test_mono/generated/inspect_derived_nested_record_string.txt @@ -231,67 +231,67 @@ procedure Inspect.63 (Inspect.295, Inspect.291): procedure Inspect.64 (Inspect.297): ret Inspect.297; -procedure List.103 (#Derived_gen.33, #Derived_gen.34, #Derived_gen.35, #Derived_gen.36, #Derived_gen.37): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : {Str, Str} = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : {Str, Int1} = CallByName Inspect.229 List.179 List.703; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.33, #Derived_gen.34, #Derived_gen.35, #Derived_gen.36, #Derived_gen.37): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : {Str, Str} = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : {Str, Int1} = CallByName Inspect.229 List.180 List.711; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.33; - jump List.697 #Derived_gen.33 #Derived_gen.34 #Derived_gen.35 #Derived_gen.36 #Derived_gen.37; + jump List.705 #Derived_gen.33 #Derived_gen.34 #Derived_gen.35 #Derived_gen.36 #Derived_gen.37; -procedure List.103 (#Derived_gen.38, #Derived_gen.39, #Derived_gen.40, #Derived_gen.41, #Derived_gen.42): - joinpoint List.709 List.178 List.179 List.180 List.181 List.182: - let List.711 : Int1 = CallByName Num.22 List.181 List.182; - if List.711 then - let List.715 : {Str, Str} = CallByName List.66 List.178 List.181; - inc List.715; - let List.183 : {Str, Int1} = CallByName Inspect.229 List.179 List.715; - let List.714 : U64 = 1i64; - let List.713 : U64 = CallByName Num.51 List.181 List.714; - jump List.709 List.178 List.183 List.180 List.713 List.182; +procedure List.104 (#Derived_gen.38, #Derived_gen.39, #Derived_gen.40, #Derived_gen.41, #Derived_gen.42): + joinpoint List.717 List.179 List.180 List.181 List.182 List.183: + let List.719 : Int1 = CallByName Num.22 List.182 List.183; + if List.719 then + let List.723 : {Str, Str} = CallByName List.66 List.179 List.182; + inc List.723; + let List.184 : {Str, Int1} = CallByName Inspect.229 List.180 List.723; + let List.722 : U64 = 1i64; + let List.721 : U64 = CallByName Num.51 List.182 List.722; + jump List.717 List.179 List.184 List.181 List.721 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.38; - jump List.709 #Derived_gen.38 #Derived_gen.39 #Derived_gen.40 #Derived_gen.41 #Derived_gen.42; + jump List.717 #Derived_gen.38 #Derived_gen.39 #Derived_gen.40 #Derived_gen.41 #Derived_gen.42; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : {Str, Int1} = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : {Str, Int1} = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; -procedure List.18 (List.175, List.176, List.177): - let List.707 : U64 = 0i64; - let List.708 : U64 = CallByName List.6 List.175; - let List.706 : {Str, Int1} = CallByName List.103 List.175 List.176 List.177 List.707 List.708; - ret List.706; +procedure List.18 (List.176, List.177, List.178): + let List.715 : U64 = 0i64; + let List.716 : U64 = CallByName List.6 List.176; + let List.714 : {Str, Int1} = CallByName List.104 List.176 List.177 List.178 List.715 List.716; + ret List.714; procedure List.6 (#Attr.2): - let List.705 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.705; + let List.713 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.713; procedure List.6 (#Attr.2): - let List.717 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.717; + let List.725 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.725; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure List.66 (#Attr.2, #Attr.3): - let List.716 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.716; + let List.724 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.724; procedure Num.20 (#Attr.2, #Attr.3): let Num.296 : U64 = lowlevel NumSub #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/inspect_derived_record.txt b/crates/compiler/test_mono/generated/inspect_derived_record.txt index 353c816d1c..b42edbbc6a 100644 --- a/crates/compiler/test_mono/generated/inspect_derived_record.txt +++ b/crates/compiler/test_mono/generated/inspect_derived_record.txt @@ -150,36 +150,36 @@ procedure Inspect.63 (Inspect.295, Inspect.291): procedure Inspect.64 (Inspect.297): ret Inspect.297; -procedure List.103 (#Derived_gen.16, #Derived_gen.17, #Derived_gen.18, #Derived_gen.19, #Derived_gen.20): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : {[C I64, C Decimal], Str} = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : {Str, Int1} = CallByName Inspect.229 List.179 List.703; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.16, #Derived_gen.17, #Derived_gen.18, #Derived_gen.19, #Derived_gen.20): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : {[C I64, C Decimal], Str} = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : {Str, Int1} = CallByName Inspect.229 List.180 List.711; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.16; - jump List.697 #Derived_gen.16 #Derived_gen.17 #Derived_gen.18 #Derived_gen.19 #Derived_gen.20; + jump List.705 #Derived_gen.16 #Derived_gen.17 #Derived_gen.18 #Derived_gen.19 #Derived_gen.20; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : {Str, Int1} = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : {Str, Int1} = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; procedure List.6 (#Attr.2): - let List.705 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.705; + let List.713 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.713; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : {[C I64, C Decimal], Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : {[C I64, C Decimal], Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure Num.22 (#Attr.2, #Attr.3): let Num.292 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/inspect_derived_record_one_field_string.txt b/crates/compiler/test_mono/generated/inspect_derived_record_one_field_string.txt index b04d7d8fe7..4ed69f3b8e 100644 --- a/crates/compiler/test_mono/generated/inspect_derived_record_one_field_string.txt +++ b/crates/compiler/test_mono/generated/inspect_derived_record_one_field_string.txt @@ -156,36 +156,36 @@ procedure Inspect.63 (Inspect.295, Inspect.291): procedure Inspect.64 (Inspect.297): ret Inspect.297; -procedure List.103 (#Derived_gen.21, #Derived_gen.22, #Derived_gen.23, #Derived_gen.24, #Derived_gen.25): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : {Str, Str} = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : {Str, Int1} = CallByName Inspect.229 List.179 List.703; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.21, #Derived_gen.22, #Derived_gen.23, #Derived_gen.24, #Derived_gen.25): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : {Str, Str} = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : {Str, Int1} = CallByName Inspect.229 List.180 List.711; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.21; - jump List.697 #Derived_gen.21 #Derived_gen.22 #Derived_gen.23 #Derived_gen.24 #Derived_gen.25; + jump List.705 #Derived_gen.21 #Derived_gen.22 #Derived_gen.23 #Derived_gen.24 #Derived_gen.25; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : {Str, Int1} = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : {Str, Int1} = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; procedure List.6 (#Attr.2): - let List.705 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.705; + let List.713 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.713; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure Num.20 (#Attr.2, #Attr.3): let Num.294 : U64 = lowlevel NumSub #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/inspect_derived_record_two_field_strings.txt b/crates/compiler/test_mono/generated/inspect_derived_record_two_field_strings.txt index 86626aa630..da3786caa6 100644 --- a/crates/compiler/test_mono/generated/inspect_derived_record_two_field_strings.txt +++ b/crates/compiler/test_mono/generated/inspect_derived_record_two_field_strings.txt @@ -163,36 +163,36 @@ procedure Inspect.63 (Inspect.295, Inspect.291): procedure Inspect.64 (Inspect.297): ret Inspect.297; -procedure List.103 (#Derived_gen.25, #Derived_gen.26, #Derived_gen.27, #Derived_gen.28, #Derived_gen.29): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : {Str, Str} = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : {Str, Int1} = CallByName Inspect.229 List.179 List.703; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.25, #Derived_gen.26, #Derived_gen.27, #Derived_gen.28, #Derived_gen.29): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : {Str, Str} = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : {Str, Int1} = CallByName Inspect.229 List.180 List.711; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.25; - jump List.697 #Derived_gen.25 #Derived_gen.26 #Derived_gen.27 #Derived_gen.28 #Derived_gen.29; + jump List.705 #Derived_gen.25 #Derived_gen.26 #Derived_gen.27 #Derived_gen.28 #Derived_gen.29; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : {Str, Int1} = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : {Str, Int1} = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; procedure List.6 (#Attr.2): - let List.705 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.705; + let List.713 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.713; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : {Str, Str} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure Num.20 (#Attr.2, #Attr.3): let Num.294 : U64 = lowlevel NumSub #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/inspect_derived_tag_one_field_string.txt b/crates/compiler/test_mono/generated/inspect_derived_tag_one_field_string.txt index d67bf0e81f..dc27611f9d 100644 --- a/crates/compiler/test_mono/generated/inspect_derived_tag_one_field_string.txt +++ b/crates/compiler/test_mono/generated/inspect_derived_tag_one_field_string.txt @@ -156,43 +156,43 @@ procedure Inspect.63 (Inspect.295, Inspect.291): procedure Inspect.64 (Inspect.297): ret Inspect.297; -procedure List.1 (List.122): - let List.707 : U64 = CallByName List.6 List.122; - let List.708 : U64 = 0i64; - let List.706 : Int1 = CallByName Bool.9 List.707 List.708; - ret List.706; +procedure List.1 (List.123): + let List.715 : U64 = CallByName List.6 List.123; + let List.716 : U64 = 0i64; + let List.714 : Int1 = CallByName Bool.9 List.715 List.716; + ret List.714; -procedure List.103 (#Derived_gen.21, #Derived_gen.22, #Derived_gen.23, #Derived_gen.24, #Derived_gen.25): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : Str = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : Str = CallByName Inspect.207 List.179 List.703; - dec List.703; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.21, #Derived_gen.22, #Derived_gen.23, #Derived_gen.24, #Derived_gen.25): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : Str = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : Str = CallByName Inspect.207 List.180 List.711; + dec List.711; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.21; - jump List.697 #Derived_gen.21 #Derived_gen.22 #Derived_gen.23 #Derived_gen.24 #Derived_gen.25; + jump List.705 #Derived_gen.21 #Derived_gen.22 #Derived_gen.23 #Derived_gen.24 #Derived_gen.25; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : Str = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : Str = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; procedure List.6 (#Attr.2): - let List.705 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.705; + let List.713 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.713; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure Num.20 (#Attr.2, #Attr.3): let Num.294 : U64 = lowlevel NumSub #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/inspect_derived_tag_two_payloads_string.txt b/crates/compiler/test_mono/generated/inspect_derived_tag_two_payloads_string.txt index 956dc01742..6966e5af01 100644 --- a/crates/compiler/test_mono/generated/inspect_derived_tag_two_payloads_string.txt +++ b/crates/compiler/test_mono/generated/inspect_derived_tag_two_payloads_string.txt @@ -159,43 +159,43 @@ procedure Inspect.63 (Inspect.295, Inspect.291): procedure Inspect.64 (Inspect.297): ret Inspect.297; -procedure List.1 (List.122): - let List.707 : U64 = CallByName List.6 List.122; - let List.708 : U64 = 0i64; - let List.706 : Int1 = CallByName Bool.9 List.707 List.708; - ret List.706; +procedure List.1 (List.123): + let List.715 : U64 = CallByName List.6 List.123; + let List.716 : U64 = 0i64; + let List.714 : Int1 = CallByName Bool.9 List.715 List.716; + ret List.714; -procedure List.103 (#Derived_gen.22, #Derived_gen.23, #Derived_gen.24, #Derived_gen.25, #Derived_gen.26): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : Str = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : Str = CallByName Inspect.207 List.179 List.703; - dec List.703; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.22, #Derived_gen.23, #Derived_gen.24, #Derived_gen.25, #Derived_gen.26): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : Str = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : Str = CallByName Inspect.207 List.180 List.711; + dec List.711; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.22; - jump List.697 #Derived_gen.22 #Derived_gen.23 #Derived_gen.24 #Derived_gen.25 #Derived_gen.26; + jump List.705 #Derived_gen.22 #Derived_gen.23 #Derived_gen.24 #Derived_gen.25 #Derived_gen.26; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : Str = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : Str = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; procedure List.6 (#Attr.2): - let List.705 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.705; + let List.713 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.713; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure Num.20 (#Attr.2, #Attr.3): let Num.294 : U64 = lowlevel NumSub #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/ir_int_add.txt b/crates/compiler/test_mono/generated/ir_int_add.txt index e031b290c9..d907d18d59 100644 --- a/crates/compiler/test_mono/generated/ir_int_add.txt +++ b/crates/compiler/test_mono/generated/ir_int_add.txt @@ -1,6 +1,6 @@ procedure List.6 (#Attr.2): - let List.694 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.694; + let List.702 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.702; procedure Num.19 (#Attr.2, #Attr.3): let Num.291 : U64 = lowlevel NumAdd #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/issue_2583_specialize_errors_behind_unified_branches.txt b/crates/compiler/test_mono/generated/issue_2583_specialize_errors_behind_unified_branches.txt index 1b2f3a6d4e..75acdc95f9 100644 --- a/crates/compiler/test_mono/generated/issue_2583_specialize_errors_behind_unified_branches.txt +++ b/crates/compiler/test_mono/generated/issue_2583_specialize_errors_behind_unified_branches.txt @@ -6,40 +6,40 @@ procedure Bool.9 (#Attr.2, #Attr.3): let Bool.22 : Int1 = lowlevel Eq #Attr.2 #Attr.3; ret Bool.22; -procedure List.2 (List.123, List.124): - let List.708 : U64 = CallByName List.6 List.123; - let List.704 : Int1 = CallByName Num.22 List.124 List.708; - if List.704 then - let List.706 : I64 = CallByName List.66 List.123 List.124; - let List.705 : [C {}, C I64] = TagId(1) List.706; - ret List.705; +procedure List.2 (List.124, List.125): + let List.716 : U64 = CallByName List.6 List.124; + let List.712 : Int1 = CallByName Num.22 List.125 List.716; + if List.712 then + let List.714 : I64 = CallByName List.66 List.124 List.125; + let List.713 : [C {}, C I64] = TagId(1) List.714; + ret List.713; else - let List.703 : {} = Struct {}; - let List.702 : [C {}, C I64] = TagId(0) List.703; - ret List.702; + let List.711 : {} = Struct {}; + let List.710 : [C {}, C I64] = TagId(0) List.711; + ret List.710; procedure List.6 (#Attr.2): - let List.709 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.709; + let List.717 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.717; procedure List.66 (#Attr.2, #Attr.3): - let List.707 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.707; + let List.715 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.715; -procedure List.9 (List.404): - let List.701 : U64 = 0i64; - let List.694 : [C {}, C I64] = CallByName List.2 List.404 List.701; - let List.698 : U8 = 1i64; - let List.699 : U8 = GetTagId List.694; - let List.700 : Int1 = lowlevel Eq List.698 List.699; - if List.700 then - let List.405 : I64 = UnionAtIndex (Id 1) (Index 0) List.694; - let List.695 : [C Int1, C I64] = TagId(1) List.405; - ret List.695; +procedure List.9 (List.405): + let List.709 : U64 = 0i64; + let List.702 : [C {}, C I64] = CallByName List.2 List.405 List.709; + let List.706 : U8 = 1i64; + let List.707 : U8 = GetTagId List.702; + let List.708 : Int1 = lowlevel Eq List.706 List.707; + if List.708 then + let List.406 : I64 = UnionAtIndex (Id 1) (Index 0) List.702; + let List.703 : [C Int1, C I64] = TagId(1) List.406; + ret List.703; else - let List.697 : Int1 = true; - let List.696 : [C Int1, C I64] = TagId(0) List.697; - ret List.696; + let List.705 : Int1 = true; + let List.704 : [C Int1, C I64] = TagId(0) List.705; + ret List.704; procedure Num.22 (#Attr.2, #Attr.3): let Num.289 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/issue_4770.txt b/crates/compiler/test_mono/generated/issue_4770.txt index 58d24d3f86..93059daa76 100644 --- a/crates/compiler/test_mono/generated/issue_4770.txt +++ b/crates/compiler/test_mono/generated/issue_4770.txt @@ -6,118 +6,118 @@ procedure Bool.2 (): let Bool.22 : Int1 = true; ret Bool.22; -procedure List.109 (Bool.30, Bool.31, Bool.32, Bool.33, Bool.34, Bool.35): - joinpoint List.732 List.303 List.304 List.305 List.306 List.307 List.308: - let List.734 : Int1 = CallByName Num.22 List.307 List.308; - if List.734 then - let List.740 : [C I64, C List *self] = CallByName List.66 List.303 List.307; - inc List.740; - let List.741 : [C I64, C List *self] = CallByName List.66 List.304 List.307; - inc List.741; - let List.309 : {[C I64, C List *self], [C I64, C List *self]} = CallByName Test.15 List.740 List.741; - let List.736 : List {[C I64, C List *self], [C I64, C List *self]} = CallByName List.71 List.305 List.309; - let List.738 : U64 = 1i64; - let List.737 : U64 = CallByName Num.51 List.307 List.738; - jump List.732 List.303 List.304 List.736 List.306 List.737 List.308; +procedure List.110 (Bool.30, Bool.31, Bool.32, Bool.33, Bool.34, Bool.35): + joinpoint List.740 List.304 List.305 List.306 List.307 List.308 List.309: + let List.742 : Int1 = CallByName Num.22 List.308 List.309; + if List.742 then + let List.748 : [C I64, C List *self] = CallByName List.66 List.304 List.308; + inc List.748; + let List.749 : [C I64, C List *self] = CallByName List.66 List.305 List.308; + inc List.749; + let List.310 : {[C I64, C List *self], [C I64, C List *self]} = CallByName Test.15 List.748 List.749; + let List.744 : List {[C I64, C List *self], [C I64, C List *self]} = CallByName List.71 List.306 List.310; + let List.746 : U64 = 1i64; + let List.745 : U64 = CallByName Num.51 List.308 List.746; + jump List.740 List.304 List.305 List.744 List.307 List.745 List.309; else dec List.304; - dec List.303; - ret List.305; + dec List.305; + ret List.306; in inc Bool.30; inc Bool.31; - jump List.732 Bool.30 Bool.31 Bool.32 Bool.33 Bool.34 Bool.35; + jump List.740 Bool.30 Bool.31 Bool.32 Bool.33 Bool.34 Bool.35; -procedure List.119 (List.582, List.583, List.584): - let List.708 : U64 = 0i64; - let List.709 : U64 = CallByName List.6 List.582; - let List.707 : [C {}, C {}] = CallByName List.80 List.582 List.583 List.584 List.708 List.709; - ret List.707; +procedure List.120 (List.590, List.591, List.592): + let List.716 : U64 = 0i64; + let List.717 : U64 = CallByName List.6 List.590; + let List.715 : [C {}, C {}] = CallByName List.80 List.590 List.591 List.592 List.716 List.717; + ret List.715; -procedure List.23 (List.299, List.300, List.301): - let List.744 : U64 = CallByName List.6 List.299; - let List.745 : U64 = CallByName List.6 List.300; - let List.302 : U64 = CallByName Num.148 List.744 List.745; - let List.730 : List {[C I64, C List *self], [C I64, C List *self]} = CallByName List.68 List.302; - let List.731 : U64 = 0i64; - let List.729 : List {[C I64, C List *self], [C I64, C List *self]} = CallByName List.109 List.299 List.300 List.730 List.301 List.731 List.302; - ret List.729; +procedure List.23 (List.300, List.301, List.302): + let List.752 : U64 = CallByName List.6 List.300; + let List.753 : U64 = CallByName List.6 List.301; + let List.303 : U64 = CallByName Num.148 List.752 List.753; + let List.738 : List {[C I64, C List *self], [C I64, C List *self]} = CallByName List.68 List.303; + let List.739 : U64 = 0i64; + let List.737 : List {[C I64, C List *self], [C I64, C List *self]} = CallByName List.110 List.300 List.301 List.738 List.302 List.739 List.303; + ret List.737; -procedure List.251 (List.696, List.252, List.250): - let List.726 : Int1 = CallByName Test.1 List.252; - if List.726 then - let List.728 : {} = Struct {}; - let List.727 : [C {}, C {}] = TagId(1) List.728; - ret List.727; +procedure List.252 (List.704, List.253, List.251): + let List.734 : Int1 = CallByName Test.1 List.253; + if List.734 then + let List.736 : {} = Struct {}; + let List.735 : [C {}, C {}] = TagId(1) List.736; + ret List.735; else - let List.725 : {} = Struct {}; - let List.724 : [C {}, C {}] = TagId(0) List.725; - ret List.724; + let List.733 : {} = Struct {}; + let List.732 : [C {}, C {}] = TagId(0) List.733; + ret List.732; -procedure List.56 (List.249, List.250): - let List.705 : {} = Struct {}; - let List.697 : [C {}, C {}] = CallByName List.119 List.249 List.705 List.250; - let List.702 : U8 = 1i64; - let List.703 : U8 = GetTagId List.697; - let List.704 : Int1 = lowlevel Eq List.702 List.703; - if List.704 then - let List.698 : Int1 = CallByName Bool.2; - ret List.698; +procedure List.56 (List.250, List.251): + let List.713 : {} = Struct {}; + let List.705 : [C {}, C {}] = CallByName List.120 List.250 List.713 List.251; + let List.710 : U8 = 1i64; + let List.711 : U8 = GetTagId List.705; + let List.712 : Int1 = lowlevel Eq List.710 List.711; + if List.712 then + let List.706 : Int1 = CallByName Bool.2; + ret List.706; else - let List.699 : Int1 = CallByName Bool.1; - ret List.699; + let List.707 : Int1 = CallByName Bool.1; + ret List.707; procedure List.6 (#Attr.2): - let List.695 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.695; + let List.703 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.703; procedure List.6 (#Attr.2): - let List.723 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.723; + let List.731 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.731; procedure List.66 (#Attr.2, #Attr.3): - let List.722 : {[C I64, C List *self], [C I64, C List *self]} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.722; + let List.730 : {[C I64, C List *self], [C I64, C List *self]} = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.730; procedure List.66 (#Attr.2, #Attr.3): - let List.742 : [C I64, C List *self] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.742; + let List.750 : [C I64, C List *self] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.750; procedure List.68 (#Attr.2): - let List.743 : List {[C I64, C List *self], [C I64, C List *self]} = lowlevel ListWithCapacity #Attr.2; - ret List.743; + let List.751 : List {[C I64, C List *self], [C I64, C List *self]} = lowlevel ListWithCapacity #Attr.2; + ret List.751; procedure List.71 (#Attr.2, #Attr.3): - let List.739 : List {[C I64, C List *self], [C I64, C List *self]} = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.739; + let List.747 : List {[C I64, C List *self], [C I64, C List *self]} = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.747; procedure List.80 (Bool.25, Bool.26, Bool.27, Bool.28, Bool.29): - joinpoint List.710 List.585 List.586 List.587 List.588 List.589: - let List.712 : Int1 = CallByName Num.22 List.588 List.589; - if List.712 then - let List.721 : {[C I64, C List *self], [C I64, C List *self]} = CallByName List.66 List.585 List.588; - inc List.721; - let List.713 : [C {}, C {}] = CallByName List.251 List.586 List.721 List.587; - let List.718 : U8 = 1i64; - let List.719 : U8 = GetTagId List.713; - let List.720 : Int1 = lowlevel Eq List.718 List.719; - if List.720 then - let List.590 : {} = UnionAtIndex (Id 1) (Index 0) List.713; - let List.716 : U64 = 1i64; - let List.715 : U64 = CallByName Num.51 List.588 List.716; - jump List.710 List.585 List.590 List.587 List.715 List.589; + joinpoint List.718 List.593 List.594 List.595 List.596 List.597: + let List.720 : Int1 = CallByName Num.22 List.596 List.597; + if List.720 then + let List.729 : {[C I64, C List *self], [C I64, C List *self]} = CallByName List.66 List.593 List.596; + inc List.729; + let List.721 : [C {}, C {}] = CallByName List.252 List.594 List.729 List.595; + let List.726 : U8 = 1i64; + let List.727 : U8 = GetTagId List.721; + let List.728 : Int1 = lowlevel Eq List.726 List.727; + if List.728 then + let List.598 : {} = UnionAtIndex (Id 1) (Index 0) List.721; + let List.724 : U64 = 1i64; + let List.723 : U64 = CallByName Num.51 List.596 List.724; + jump List.718 List.593 List.598 List.595 List.723 List.597; else - dec List.585; - let List.591 : {} = UnionAtIndex (Id 0) (Index 0) List.713; - let List.717 : [C {}, C {}] = TagId(0) List.591; - ret List.717; + dec List.593; + let List.599 : {} = UnionAtIndex (Id 0) (Index 0) List.721; + let List.725 : [C {}, C {}] = TagId(0) List.599; + ret List.725; else - dec List.585; - let List.711 : [C {}, C {}] = TagId(1) List.586; - ret List.711; + dec List.593; + let List.719 : [C {}, C {}] = TagId(1) List.594; + ret List.719; in inc Bool.25; - jump List.710 Bool.25 Bool.26 Bool.27 Bool.28 Bool.29; + jump List.718 Bool.25 Bool.26 Bool.27 Bool.28 Bool.29; procedure Num.148 (Num.232, Num.233): let Num.296 : Int1 = CallByName Num.22 Num.232 Num.233; diff --git a/crates/compiler/test_mono/generated/layout_cache_structure_with_multiple_recursive_structures.txt b/crates/compiler/test_mono/generated/layout_cache_structure_with_multiple_recursive_structures.txt index 77ef060092..2d03765dea 100644 --- a/crates/compiler/test_mono/generated/layout_cache_structure_with_multiple_recursive_structures.txt +++ b/crates/compiler/test_mono/generated/layout_cache_structure_with_multiple_recursive_structures.txt @@ -1,33 +1,33 @@ -procedure List.103 (Bool.21, Bool.22, Bool.23, Bool.24, Bool.25): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : [C *self, ] = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : [, C {[C *self, ], *self}] = CallByName Test.7 List.179 List.703; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (Bool.21, Bool.22, Bool.23, Bool.24, Bool.25): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : [C *self, ] = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : [, C {[C *self, ], *self}] = CallByName Test.7 List.180 List.711; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc Bool.21; - jump List.697 Bool.21 Bool.22 Bool.23 Bool.24 Bool.25; + jump List.705 Bool.21 Bool.22 Bool.23 Bool.24 Bool.25; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : [, C {[C *self, ], *self}] = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : [, C {[C *self, ], *self}] = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; procedure List.6 (#Attr.2): - let List.705 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.705; + let List.713 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.713; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : [C *self, ] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : [C *self, ] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure Num.22 (#Attr.2, #Attr.3): let Num.290 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/list_append.txt b/crates/compiler/test_mono/generated/list_append.txt index b1fa479ac7..5d9fa99ddd 100644 --- a/crates/compiler/test_mono/generated/list_append.txt +++ b/crates/compiler/test_mono/generated/list_append.txt @@ -1,16 +1,16 @@ -procedure List.4 (List.139, List.140): - let List.697 : U64 = 1i64; - let List.695 : List I64 = CallByName List.70 List.139 List.697; - let List.694 : List I64 = CallByName List.71 List.695 List.140; - ret List.694; +procedure List.4 (List.140, List.141): + let List.705 : U64 = 1i64; + let List.703 : List I64 = CallByName List.70 List.140 List.705; + let List.702 : List I64 = CallByName List.71 List.703 List.141; + ret List.702; procedure List.70 (#Attr.2, #Attr.3): - let List.698 : List I64 = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.698; + let List.706 : List I64 = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.706; procedure List.71 (#Attr.2, #Attr.3): - let List.696 : List I64 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.696; + let List.704 : List I64 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.704; procedure Test.0 (): let Test.2 : List I64 = Array [1i64]; diff --git a/crates/compiler/test_mono/generated/list_append_closure.txt b/crates/compiler/test_mono/generated/list_append_closure.txt index 3d34a2147e..ccb0ac56ca 100644 --- a/crates/compiler/test_mono/generated/list_append_closure.txt +++ b/crates/compiler/test_mono/generated/list_append_closure.txt @@ -1,16 +1,16 @@ -procedure List.4 (List.139, List.140): - let List.697 : U64 = 1i64; - let List.695 : List I64 = CallByName List.70 List.139 List.697; - let List.694 : List I64 = CallByName List.71 List.695 List.140; - ret List.694; +procedure List.4 (List.140, List.141): + let List.705 : U64 = 1i64; + let List.703 : List I64 = CallByName List.70 List.140 List.705; + let List.702 : List I64 = CallByName List.71 List.703 List.141; + ret List.702; procedure List.70 (#Attr.2, #Attr.3): - let List.698 : List I64 = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.698; + let List.706 : List I64 = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.706; procedure List.71 (#Attr.2, #Attr.3): - let List.696 : List I64 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.696; + let List.704 : List I64 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.704; procedure Test.1 (Test.2): let Test.6 : I64 = 42i64; diff --git a/crates/compiler/test_mono/generated/list_cannot_update_inplace.txt b/crates/compiler/test_mono/generated/list_cannot_update_inplace.txt index 968ae0be25..4127b829fb 100644 --- a/crates/compiler/test_mono/generated/list_cannot_update_inplace.txt +++ b/crates/compiler/test_mono/generated/list_cannot_update_inplace.txt @@ -1,25 +1,25 @@ -procedure List.3 (List.131, List.132, List.133): - let List.697 : {List I64, I64} = CallByName List.64 List.131 List.132 List.133; - let List.696 : List I64 = StructAtIndex 0 List.697; - ret List.696; +procedure List.3 (List.132, List.133, List.134): + let List.705 : {List I64, I64} = CallByName List.64 List.132 List.133 List.134; + let List.704 : List I64 = StructAtIndex 0 List.705; + ret List.704; procedure List.6 (#Attr.2): - let List.695 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.695; + let List.703 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.703; -procedure List.64 (List.128, List.129, List.130): - let List.702 : U64 = CallByName List.6 List.128; - let List.699 : Int1 = CallByName Num.22 List.129 List.702; - if List.699 then - let List.700 : {List I64, I64} = CallByName List.67 List.128 List.129 List.130; - ret List.700; +procedure List.64 (List.129, List.130, List.131): + let List.710 : U64 = CallByName List.6 List.129; + let List.707 : Int1 = CallByName Num.22 List.130 List.710; + if List.707 then + let List.708 : {List I64, I64} = CallByName List.67 List.129 List.130 List.131; + ret List.708; else - let List.698 : {List I64, I64} = Struct {List.128, List.130}; - ret List.698; + let List.706 : {List I64, I64} = Struct {List.129, List.131}; + ret List.706; procedure List.67 (#Attr.2, #Attr.3, #Attr.4): - let List.701 : {List I64, I64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; - ret List.701; + let List.709 : {List I64, I64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; + ret List.709; procedure Num.19 (#Attr.2, #Attr.3): let Num.289 : U64 = lowlevel NumAdd #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/list_get.txt b/crates/compiler/test_mono/generated/list_get.txt index d198ffa86d..54997cd57f 100644 --- a/crates/compiler/test_mono/generated/list_get.txt +++ b/crates/compiler/test_mono/generated/list_get.txt @@ -1,22 +1,22 @@ -procedure List.2 (List.123, List.124): - let List.700 : U64 = CallByName List.6 List.123; - let List.696 : Int1 = CallByName Num.22 List.124 List.700; - if List.696 then - let List.698 : I64 = CallByName List.66 List.123 List.124; - let List.697 : [C {}, C I64] = TagId(1) List.698; - ret List.697; +procedure List.2 (List.124, List.125): + let List.708 : U64 = CallByName List.6 List.124; + let List.704 : Int1 = CallByName Num.22 List.125 List.708; + if List.704 then + let List.706 : I64 = CallByName List.66 List.124 List.125; + let List.705 : [C {}, C I64] = TagId(1) List.706; + ret List.705; else - let List.695 : {} = Struct {}; - let List.694 : [C {}, C I64] = TagId(0) List.695; - ret List.694; + let List.703 : {} = Struct {}; + let List.702 : [C {}, C I64] = TagId(0) List.703; + ret List.702; procedure List.6 (#Attr.2): - let List.701 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.701; + let List.709 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.709; procedure List.66 (#Attr.2, #Attr.3): - let List.699 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.699; + let List.707 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.707; procedure Num.22 (#Attr.2, #Attr.3): let Num.289 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/list_len.txt b/crates/compiler/test_mono/generated/list_len.txt index 97fe0cbbe3..8162fa185e 100644 --- a/crates/compiler/test_mono/generated/list_len.txt +++ b/crates/compiler/test_mono/generated/list_len.txt @@ -1,10 +1,10 @@ procedure List.6 (#Attr.2): - let List.694 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.694; + let List.702 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.702; procedure List.6 (#Attr.2): - let List.695 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.695; + let List.703 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.703; procedure Num.19 (#Attr.2, #Attr.3): let Num.289 : U64 = lowlevel NumAdd #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/list_map_closure_borrows.txt b/crates/compiler/test_mono/generated/list_map_closure_borrows.txt index 8f4583b6db..3ac12a39bc 100644 --- a/crates/compiler/test_mono/generated/list_map_closure_borrows.txt +++ b/crates/compiler/test_mono/generated/list_map_closure_borrows.txt @@ -1,66 +1,66 @@ -procedure List.103 (Bool.21, Bool.22, Bool.23, Bool.24, Bool.25): - joinpoint List.708 List.178 List.179 List.180 List.181 List.182: - let List.710 : Int1 = CallByName Num.22 List.181 List.182; - if List.710 then - let List.714 : Str = CallByName List.66 List.178 List.181; - inc List.714; - let List.183 : List Str = CallByName List.296 List.179 List.714 List.180; - dec List.714; - let List.713 : U64 = 1i64; - let List.712 : U64 = CallByName Num.51 List.181 List.713; - jump List.708 List.178 List.183 List.180 List.712 List.182; +procedure List.104 (Bool.21, Bool.22, Bool.23, Bool.24, Bool.25): + joinpoint List.716 List.179 List.180 List.181 List.182 List.183: + let List.718 : Int1 = CallByName Num.22 List.182 List.183; + if List.718 then + let List.722 : Str = CallByName List.66 List.179 List.182; + inc List.722; + let List.184 : List Str = CallByName List.297 List.180 List.722 List.181; + dec List.722; + let List.721 : U64 = 1i64; + let List.720 : U64 = CallByName Num.51 List.182 List.721; + jump List.716 List.179 List.184 List.181 List.720 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc Bool.21; - jump List.708 Bool.21 Bool.22 Bool.23 Bool.24 Bool.25; + jump List.716 Bool.21 Bool.22 Bool.23 Bool.24 Bool.25; -procedure List.18 (List.175, List.176, List.177): - let List.706 : U64 = 0i64; - let List.707 : U64 = CallByName List.6 List.175; - let List.705 : List Str = CallByName List.103 List.175 List.176 List.177 List.706 List.707; - ret List.705; +procedure List.18 (List.176, List.177, List.178): + let List.714 : U64 = 0i64; + let List.715 : U64 = CallByName List.6 List.176; + let List.713 : List Str = CallByName List.104 List.176 List.177 List.178 List.714 List.715; + ret List.713; -procedure List.2 (List.123, List.124): - let List.700 : U64 = CallByName List.6 List.123; - let List.696 : Int1 = CallByName Num.22 List.124 List.700; - if List.696 then - let List.698 : Str = CallByName List.66 List.123 List.124; - inc List.698; - let List.697 : [C {}, C Str] = TagId(1) List.698; - ret List.697; +procedure List.2 (List.124, List.125): + let List.708 : U64 = CallByName List.6 List.124; + let List.704 : Int1 = CallByName Num.22 List.125 List.708; + if List.704 then + let List.706 : Str = CallByName List.66 List.124 List.125; + inc List.706; + let List.705 : [C {}, C Str] = TagId(1) List.706; + ret List.705; else - let List.695 : {} = Struct {}; - let List.694 : [C {}, C Str] = TagId(0) List.695; - ret List.694; + let List.703 : {} = Struct {}; + let List.702 : [C {}, C Str] = TagId(0) List.703; + ret List.702; -procedure List.296 (List.297, List.298, List.294): - let List.717 : Str = CallByName Test.3 List.298; - let List.716 : List Str = CallByName List.71 List.297 List.717; - ret List.716; +procedure List.297 (List.298, List.299, List.295): + let List.725 : Str = CallByName Test.3 List.299; + let List.724 : List Str = CallByName List.71 List.298 List.725; + ret List.724; -procedure List.5 (List.293, List.294): - let List.295 : U64 = CallByName List.6 List.293; - let List.703 : List Str = CallByName List.68 List.295; - let List.702 : List Str = CallByName List.18 List.293 List.703 List.294; - ret List.702; +procedure List.5 (List.294, List.295): + let List.296 : U64 = CallByName List.6 List.294; + let List.711 : List Str = CallByName List.68 List.296; + let List.710 : List Str = CallByName List.18 List.294 List.711 List.295; + ret List.710; procedure List.6 (#Attr.2): - let List.701 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.701; + let List.709 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.709; procedure List.66 (#Attr.2, #Attr.3): - let List.699 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.699; + let List.707 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.707; procedure List.68 (#Attr.2): - let List.719 : List Str = lowlevel ListWithCapacity #Attr.2; - ret List.719; + let List.727 : List Str = lowlevel ListWithCapacity #Attr.2; + ret List.727; procedure List.71 (#Attr.2, #Attr.3): - let List.718 : List Str = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.718; + let List.726 : List Str = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.726; procedure Num.22 (#Attr.2, #Attr.3): let Num.290 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/list_map_closure_owns.txt b/crates/compiler/test_mono/generated/list_map_closure_owns.txt index d6c658c0d9..a72a8f4828 100644 --- a/crates/compiler/test_mono/generated/list_map_closure_owns.txt +++ b/crates/compiler/test_mono/generated/list_map_closure_owns.txt @@ -1,65 +1,65 @@ -procedure List.103 (Bool.21, Bool.22, Bool.23, Bool.24, Bool.25): - joinpoint List.708 List.178 List.179 List.180 List.181 List.182: - let List.710 : Int1 = CallByName Num.22 List.181 List.182; - if List.710 then - let List.714 : Str = CallByName List.66 List.178 List.181; - inc List.714; - let List.183 : List Str = CallByName List.296 List.179 List.714 List.180; - let List.713 : U64 = 1i64; - let List.712 : U64 = CallByName Num.51 List.181 List.713; - jump List.708 List.178 List.183 List.180 List.712 List.182; +procedure List.104 (Bool.21, Bool.22, Bool.23, Bool.24, Bool.25): + joinpoint List.716 List.179 List.180 List.181 List.182 List.183: + let List.718 : Int1 = CallByName Num.22 List.182 List.183; + if List.718 then + let List.722 : Str = CallByName List.66 List.179 List.182; + inc List.722; + let List.184 : List Str = CallByName List.297 List.180 List.722 List.181; + let List.721 : U64 = 1i64; + let List.720 : U64 = CallByName Num.51 List.182 List.721; + jump List.716 List.179 List.184 List.181 List.720 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc Bool.21; - jump List.708 Bool.21 Bool.22 Bool.23 Bool.24 Bool.25; + jump List.716 Bool.21 Bool.22 Bool.23 Bool.24 Bool.25; -procedure List.18 (List.175, List.176, List.177): - let List.706 : U64 = 0i64; - let List.707 : U64 = CallByName List.6 List.175; - let List.705 : List Str = CallByName List.103 List.175 List.176 List.177 List.706 List.707; - ret List.705; +procedure List.18 (List.176, List.177, List.178): + let List.714 : U64 = 0i64; + let List.715 : U64 = CallByName List.6 List.176; + let List.713 : List Str = CallByName List.104 List.176 List.177 List.178 List.714 List.715; + ret List.713; -procedure List.2 (List.123, List.124): - let List.700 : U64 = CallByName List.6 List.123; - let List.696 : Int1 = CallByName Num.22 List.124 List.700; - if List.696 then - let List.698 : Str = CallByName List.66 List.123 List.124; - inc List.698; - let List.697 : [C {}, C Str] = TagId(1) List.698; - ret List.697; +procedure List.2 (List.124, List.125): + let List.708 : U64 = CallByName List.6 List.124; + let List.704 : Int1 = CallByName Num.22 List.125 List.708; + if List.704 then + let List.706 : Str = CallByName List.66 List.124 List.125; + inc List.706; + let List.705 : [C {}, C Str] = TagId(1) List.706; + ret List.705; else - let List.695 : {} = Struct {}; - let List.694 : [C {}, C Str] = TagId(0) List.695; - ret List.694; + let List.703 : {} = Struct {}; + let List.702 : [C {}, C Str] = TagId(0) List.703; + ret List.702; -procedure List.296 (List.297, List.298, List.294): - let List.717 : Str = CallByName Test.3 List.298; - let List.716 : List Str = CallByName List.71 List.297 List.717; - ret List.716; +procedure List.297 (List.298, List.299, List.295): + let List.725 : Str = CallByName Test.3 List.299; + let List.724 : List Str = CallByName List.71 List.298 List.725; + ret List.724; -procedure List.5 (List.293, List.294): - let List.295 : U64 = CallByName List.6 List.293; - let List.703 : List Str = CallByName List.68 List.295; - let List.702 : List Str = CallByName List.18 List.293 List.703 List.294; - ret List.702; +procedure List.5 (List.294, List.295): + let List.296 : U64 = CallByName List.6 List.294; + let List.711 : List Str = CallByName List.68 List.296; + let List.710 : List Str = CallByName List.18 List.294 List.711 List.295; + ret List.710; procedure List.6 (#Attr.2): - let List.701 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.701; + let List.709 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.709; procedure List.66 (#Attr.2, #Attr.3): - let List.699 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.699; + let List.707 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.707; procedure List.68 (#Attr.2): - let List.719 : List Str = lowlevel ListWithCapacity #Attr.2; - ret List.719; + let List.727 : List Str = lowlevel ListWithCapacity #Attr.2; + ret List.727; procedure List.71 (#Attr.2, #Attr.3): - let List.718 : List Str = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.718; + let List.726 : List Str = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.726; procedure Num.22 (#Attr.2, #Attr.3): let Num.290 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/list_map_take_capturing_or_noncapturing.txt b/crates/compiler/test_mono/generated/list_map_take_capturing_or_noncapturing.txt index 4d446d0a49..a284285dfe 100644 --- a/crates/compiler/test_mono/generated/list_map_take_capturing_or_noncapturing.txt +++ b/crates/compiler/test_mono/generated/list_map_take_capturing_or_noncapturing.txt @@ -1,66 +1,66 @@ -procedure List.103 (Bool.21, Bool.22, Bool.23, Bool.24, Bool.25): - joinpoint List.700 List.178 List.179 List.180 List.181 List.182: - let List.702 : Int1 = CallByName Num.22 List.181 List.182; - if List.702 then - let List.706 : U8 = CallByName List.66 List.178 List.181; - let List.183 : List U8 = CallByName List.296 List.179 List.706 List.180; - let List.705 : U64 = 1i64; - let List.704 : U64 = CallByName Num.51 List.181 List.705; - jump List.700 List.178 List.183 List.180 List.704 List.182; +procedure List.104 (Bool.21, Bool.22, Bool.23, Bool.24, Bool.25): + joinpoint List.708 List.179 List.180 List.181 List.182 List.183: + let List.710 : Int1 = CallByName Num.22 List.182 List.183; + if List.710 then + let List.714 : U8 = CallByName List.66 List.179 List.182; + let List.184 : List U8 = CallByName List.297 List.180 List.714 List.181; + let List.713 : U64 = 1i64; + let List.712 : U64 = CallByName Num.51 List.182 List.713; + jump List.708 List.179 List.184 List.181 List.712 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc Bool.21; - jump List.700 Bool.21 Bool.22 Bool.23 Bool.24 Bool.25; + jump List.708 Bool.21 Bool.22 Bool.23 Bool.24 Bool.25; -procedure List.18 (List.175, List.176, List.177): - let List.698 : U64 = 0i64; - let List.699 : U64 = CallByName List.6 List.175; - let List.697 : List U8 = CallByName List.103 List.175 List.176 List.177 List.698 List.699; - ret List.697; +procedure List.18 (List.176, List.177, List.178): + let List.706 : U64 = 0i64; + let List.707 : U64 = CallByName List.6 List.176; + let List.705 : List U8 = CallByName List.104 List.176 List.177 List.178 List.706 List.707; + ret List.705; -procedure List.296 (List.297, List.298, List.294): - let List.713 : U8 = GetTagId List.294; - joinpoint List.714 List.711: - let List.710 : List U8 = CallByName List.71 List.297 List.711; - ret List.710; +procedure List.297 (List.298, List.299, List.295): + let List.721 : U8 = GetTagId List.295; + joinpoint List.722 List.719: + let List.718 : List U8 = CallByName List.71 List.298 List.719; + ret List.718; in - switch List.713: + switch List.721: case 0: - let List.715 : U8 = CallByName Test.4 List.298 List.294; - jump List.714 List.715; + let List.723 : U8 = CallByName Test.4 List.299 List.295; + jump List.722 List.723; case 1: - let List.715 : U8 = CallByName Test.6 List.298 List.294; - jump List.714 List.715; + let List.723 : U8 = CallByName Test.6 List.299 List.295; + jump List.722 List.723; default: - let List.715 : U8 = CallByName Test.8 List.298; - jump List.714 List.715; + let List.723 : U8 = CallByName Test.8 List.299; + jump List.722 List.723; -procedure List.5 (List.293, List.294): - let List.295 : U64 = CallByName List.6 List.293; - let List.695 : List U8 = CallByName List.68 List.295; - let List.694 : List U8 = CallByName List.18 List.293 List.695 List.294; - ret List.694; +procedure List.5 (List.294, List.295): + let List.296 : U64 = CallByName List.6 List.294; + let List.703 : List U8 = CallByName List.68 List.296; + let List.702 : List U8 = CallByName List.18 List.294 List.703 List.295; + ret List.702; procedure List.6 (#Attr.2): - let List.708 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.708; - -procedure List.66 (#Attr.2, #Attr.3): - let List.707 : U8 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.707; - -procedure List.68 (#Attr.2): - let List.716 : List U8 = lowlevel ListWithCapacity #Attr.2; + let List.716 : U64 = lowlevel ListLenU64 #Attr.2; ret List.716; +procedure List.66 (#Attr.2, #Attr.3): + let List.715 : U8 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.715; + +procedure List.68 (#Attr.2): + let List.724 : List U8 = lowlevel ListWithCapacity #Attr.2; + ret List.724; + procedure List.71 (#Attr.2, #Attr.3): - let List.712 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.712; + let List.720 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.720; procedure Num.19 (#Attr.2, #Attr.3): let Num.291 : U8 = lowlevel NumAdd #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/list_pass_to_function.txt b/crates/compiler/test_mono/generated/list_pass_to_function.txt index 2e85cc2910..5b949b0b43 100644 --- a/crates/compiler/test_mono/generated/list_pass_to_function.txt +++ b/crates/compiler/test_mono/generated/list_pass_to_function.txt @@ -1,25 +1,25 @@ -procedure List.3 (List.131, List.132, List.133): - let List.695 : {List I64, I64} = CallByName List.64 List.131 List.132 List.133; - let List.694 : List I64 = StructAtIndex 0 List.695; - ret List.694; +procedure List.3 (List.132, List.133, List.134): + let List.703 : {List I64, I64} = CallByName List.64 List.132 List.133 List.134; + let List.702 : List I64 = StructAtIndex 0 List.703; + ret List.702; procedure List.6 (#Attr.2): - let List.701 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.701; + let List.709 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.709; -procedure List.64 (List.128, List.129, List.130): - let List.700 : U64 = CallByName List.6 List.128; - let List.697 : Int1 = CallByName Num.22 List.129 List.700; - if List.697 then - let List.698 : {List I64, I64} = CallByName List.67 List.128 List.129 List.130; - ret List.698; +procedure List.64 (List.129, List.130, List.131): + let List.708 : U64 = CallByName List.6 List.129; + let List.705 : Int1 = CallByName Num.22 List.130 List.708; + if List.705 then + let List.706 : {List I64, I64} = CallByName List.67 List.129 List.130 List.131; + ret List.706; else - let List.696 : {List I64, I64} = Struct {List.128, List.130}; - ret List.696; + let List.704 : {List I64, I64} = Struct {List.129, List.131}; + ret List.704; procedure List.67 (#Attr.2, #Attr.3, #Attr.4): - let List.699 : {List I64, I64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; - ret List.699; + let List.707 : {List I64, I64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; + ret List.707; procedure Num.22 (#Attr.2, #Attr.3): let Num.289 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/list_sort_asc.txt b/crates/compiler/test_mono/generated/list_sort_asc.txt index ac59bcb528..4d0c40d30e 100644 --- a/crates/compiler/test_mono/generated/list_sort_asc.txt +++ b/crates/compiler/test_mono/generated/list_sort_asc.txt @@ -1,11 +1,11 @@ procedure List.28 (#Attr.2, #Attr.3): - let List.696 : List I64 = lowlevel ListSortWith { xs: `#Attr.#arg1` } #Attr.2 Num.46 #Attr.3; - ret List.696; + let List.704 : List I64 = lowlevel ListSortWith { xs: `#Attr.#arg1` } #Attr.2 Num.46 #Attr.3; + ret List.704; -procedure List.59 (List.399): - let List.695 : {} = Struct {}; - let List.694 : List I64 = CallByName List.28 List.399 List.695; - ret List.694; +procedure List.59 (List.400): + let List.703 : {} = Struct {}; + let List.702 : List I64 = CallByName List.28 List.400 List.703; + ret List.702; procedure Num.46 (#Attr.2, #Attr.3): let Num.289 : U8 = lowlevel NumCompare #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/quicksort_swap.txt b/crates/compiler/test_mono/generated/quicksort_swap.txt index 5e2c50bcbf..fc0cabe244 100644 --- a/crates/compiler/test_mono/generated/quicksort_swap.txt +++ b/crates/compiler/test_mono/generated/quicksort_swap.txt @@ -1,41 +1,41 @@ -procedure List.2 (List.123, List.124): - let List.716 : U64 = CallByName List.6 List.123; - let List.713 : Int1 = CallByName Num.22 List.124 List.716; - if List.713 then - let List.715 : I64 = CallByName List.66 List.123 List.124; - let List.714 : [C {}, C I64] = TagId(1) List.715; - ret List.714; +procedure List.2 (List.124, List.125): + let List.724 : U64 = CallByName List.6 List.124; + let List.721 : Int1 = CallByName Num.22 List.125 List.724; + if List.721 then + let List.723 : I64 = CallByName List.66 List.124 List.125; + let List.722 : [C {}, C I64] = TagId(1) List.723; + ret List.722; else - let List.712 : {} = Struct {}; - let List.711 : [C {}, C I64] = TagId(0) List.712; - ret List.711; + let List.720 : {} = Struct {}; + let List.719 : [C {}, C I64] = TagId(0) List.720; + ret List.719; -procedure List.3 (List.131, List.132, List.133): - let List.703 : {List I64, I64} = CallByName List.64 List.131 List.132 List.133; - let List.702 : List I64 = StructAtIndex 0 List.703; - ret List.702; +procedure List.3 (List.132, List.133, List.134): + let List.711 : {List I64, I64} = CallByName List.64 List.132 List.133 List.134; + let List.710 : List I64 = StructAtIndex 0 List.711; + ret List.710; procedure List.6 (#Attr.2): - let List.701 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.701; - -procedure List.64 (List.128, List.129, List.130): - let List.700 : U64 = CallByName List.6 List.128; - let List.697 : Int1 = CallByName Num.22 List.129 List.700; - if List.697 then - let List.698 : {List I64, I64} = CallByName List.67 List.128 List.129 List.130; - ret List.698; - else - let List.696 : {List I64, I64} = Struct {List.128, List.130}; - ret List.696; - -procedure List.66 (#Attr.2, #Attr.3): - let List.709 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + let List.709 : U64 = lowlevel ListLenU64 #Attr.2; ret List.709; +procedure List.64 (List.129, List.130, List.131): + let List.708 : U64 = CallByName List.6 List.129; + let List.705 : Int1 = CallByName Num.22 List.130 List.708; + if List.705 then + let List.706 : {List I64, I64} = CallByName List.67 List.129 List.130 List.131; + ret List.706; + else + let List.704 : {List I64, I64} = Struct {List.129, List.131}; + ret List.704; + +procedure List.66 (#Attr.2, #Attr.3): + let List.717 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.717; + procedure List.67 (#Attr.2, #Attr.3, #Attr.4): - let List.699 : {List I64, I64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; - ret List.699; + let List.707 : {List I64, I64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; + ret List.707; procedure Num.22 (#Attr.2, #Attr.3): let Num.291 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/record_update.txt b/crates/compiler/test_mono/generated/record_update.txt index 734e89a620..48a4bfabf4 100644 --- a/crates/compiler/test_mono/generated/record_update.txt +++ b/crates/compiler/test_mono/generated/record_update.txt @@ -1,25 +1,25 @@ -procedure List.3 (List.131, List.132, List.133): - let List.703 : {List U64, U64} = CallByName List.64 List.131 List.132 List.133; - let List.702 : List U64 = StructAtIndex 0 List.703; - ret List.702; +procedure List.3 (List.132, List.133, List.134): + let List.711 : {List U64, U64} = CallByName List.64 List.132 List.133 List.134; + let List.710 : List U64 = StructAtIndex 0 List.711; + ret List.710; procedure List.6 (#Attr.2): - let List.701 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.701; + let List.709 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.709; -procedure List.64 (List.128, List.129, List.130): - let List.700 : U64 = CallByName List.6 List.128; - let List.697 : Int1 = CallByName Num.22 List.129 List.700; - if List.697 then - let List.698 : {List U64, U64} = CallByName List.67 List.128 List.129 List.130; - ret List.698; +procedure List.64 (List.129, List.130, List.131): + let List.708 : U64 = CallByName List.6 List.129; + let List.705 : Int1 = CallByName Num.22 List.130 List.708; + if List.705 then + let List.706 : {List U64, U64} = CallByName List.67 List.129 List.130 List.131; + ret List.706; else - let List.696 : {List U64, U64} = Struct {List.128, List.130}; - ret List.696; + let List.704 : {List U64, U64} = Struct {List.129, List.131}; + ret List.704; procedure List.67 (#Attr.2, #Attr.3, #Attr.4): - let List.699 : {List U64, U64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; - ret List.699; + let List.707 : {List U64, U64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; + ret List.707; procedure Num.22 (#Attr.2, #Attr.3): let Num.289 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/recursive_function_and_union_with_inference_hole.txt b/crates/compiler/test_mono/generated/recursive_function_and_union_with_inference_hole.txt index 2610bb61d2..0f73bab314 100644 --- a/crates/compiler/test_mono/generated/recursive_function_and_union_with_inference_hole.txt +++ b/crates/compiler/test_mono/generated/recursive_function_and_union_with_inference_hole.txt @@ -1,52 +1,52 @@ -procedure List.103 (Bool.22, Bool.23, Bool.24, Bool.25, Bool.26): - joinpoint List.700 List.178 List.179 List.180 List.181 List.182: - let List.702 : Int1 = CallByName Num.22 List.181 List.182; - if List.702 then - let List.706 : [C List *self] = CallByName List.66 List.178 List.181; - inc List.706; - let List.183 : List [C List *self] = CallByName List.296 List.179 List.706 List.180; - let List.705 : U64 = 1i64; - let List.704 : U64 = CallByName Num.51 List.181 List.705; - jump List.700 List.178 List.183 List.180 List.704 List.182; +procedure List.104 (Bool.22, Bool.23, Bool.24, Bool.25, Bool.26): + joinpoint List.708 List.179 List.180 List.181 List.182 List.183: + let List.710 : Int1 = CallByName Num.22 List.182 List.183; + if List.710 then + let List.714 : [C List *self] = CallByName List.66 List.179 List.182; + inc List.714; + let List.184 : List [C List *self] = CallByName List.297 List.180 List.714 List.181; + let List.713 : U64 = 1i64; + let List.712 : U64 = CallByName Num.51 List.182 List.713; + jump List.708 List.179 List.184 List.181 List.712 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc Bool.22; - jump List.700 Bool.22 Bool.23 Bool.24 Bool.25 Bool.26; + jump List.708 Bool.22 Bool.23 Bool.24 Bool.25 Bool.26; -procedure List.18 (List.175, List.176, List.177): - let List.698 : U64 = 0i64; - let List.699 : U64 = CallByName List.6 List.175; - let List.697 : List [C List *self] = CallByName List.103 List.175 List.176 List.177 List.698 List.699; - ret List.697; +procedure List.18 (List.176, List.177, List.178): + let List.706 : U64 = 0i64; + let List.707 : U64 = CallByName List.6 List.176; + let List.705 : List [C List *self] = CallByName List.104 List.176 List.177 List.178 List.706 List.707; + ret List.705; -procedure List.296 (List.297, List.298, List.294): - let List.711 : [C List *self] = CallByName Test.2 List.298; - let List.710 : List [C List *self] = CallByName List.71 List.297 List.711; - ret List.710; +procedure List.297 (List.298, List.299, List.295): + let List.719 : [C List *self] = CallByName Test.2 List.299; + let List.718 : List [C List *self] = CallByName List.71 List.298 List.719; + ret List.718; -procedure List.5 (List.293, List.294): - let List.295 : U64 = CallByName List.6 List.293; - let List.695 : List [C List *self] = CallByName List.68 List.295; - let List.694 : List [C List *self] = CallByName List.18 List.293 List.695 List.294; - ret List.694; +procedure List.5 (List.294, List.295): + let List.296 : U64 = CallByName List.6 List.294; + let List.703 : List [C List *self] = CallByName List.68 List.296; + let List.702 : List [C List *self] = CallByName List.18 List.294 List.703 List.295; + ret List.702; procedure List.6 (#Attr.2): - let List.708 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.708; + let List.716 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.716; procedure List.66 (#Attr.2, #Attr.3): - let List.707 : [C List *self] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.707; + let List.715 : [C List *self] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.715; procedure List.68 (#Attr.2): - let List.713 : List [C List *self] = lowlevel ListWithCapacity #Attr.2; - ret List.713; + let List.721 : List [C List *self] = lowlevel ListWithCapacity #Attr.2; + ret List.721; procedure List.71 (#Attr.2, #Attr.3): - let List.712 : List [C List *self] = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.712; + let List.720 : List [C List *self] = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.720; procedure Num.22 (#Attr.2, #Attr.3): let Num.290 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/rigids.txt b/crates/compiler/test_mono/generated/rigids.txt index b4d9885251..02ebc98f84 100644 --- a/crates/compiler/test_mono/generated/rigids.txt +++ b/crates/compiler/test_mono/generated/rigids.txt @@ -1,41 +1,41 @@ -procedure List.2 (List.123, List.124): - let List.716 : U64 = CallByName List.6 List.123; - let List.713 : Int1 = CallByName Num.22 List.124 List.716; - if List.713 then - let List.715 : I64 = CallByName List.66 List.123 List.124; - let List.714 : [C {}, C I64] = TagId(1) List.715; - ret List.714; +procedure List.2 (List.124, List.125): + let List.724 : U64 = CallByName List.6 List.124; + let List.721 : Int1 = CallByName Num.22 List.125 List.724; + if List.721 then + let List.723 : I64 = CallByName List.66 List.124 List.125; + let List.722 : [C {}, C I64] = TagId(1) List.723; + ret List.722; else - let List.712 : {} = Struct {}; - let List.711 : [C {}, C I64] = TagId(0) List.712; - ret List.711; + let List.720 : {} = Struct {}; + let List.719 : [C {}, C I64] = TagId(0) List.720; + ret List.719; -procedure List.3 (List.131, List.132, List.133): - let List.703 : {List I64, I64} = CallByName List.64 List.131 List.132 List.133; - let List.702 : List I64 = StructAtIndex 0 List.703; - ret List.702; +procedure List.3 (List.132, List.133, List.134): + let List.711 : {List I64, I64} = CallByName List.64 List.132 List.133 List.134; + let List.710 : List I64 = StructAtIndex 0 List.711; + ret List.710; procedure List.6 (#Attr.2): - let List.701 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.701; - -procedure List.64 (List.128, List.129, List.130): - let List.700 : U64 = CallByName List.6 List.128; - let List.697 : Int1 = CallByName Num.22 List.129 List.700; - if List.697 then - let List.698 : {List I64, I64} = CallByName List.67 List.128 List.129 List.130; - ret List.698; - else - let List.696 : {List I64, I64} = Struct {List.128, List.130}; - ret List.696; - -procedure List.66 (#Attr.2, #Attr.3): - let List.709 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + let List.709 : U64 = lowlevel ListLenU64 #Attr.2; ret List.709; +procedure List.64 (List.129, List.130, List.131): + let List.708 : U64 = CallByName List.6 List.129; + let List.705 : Int1 = CallByName Num.22 List.130 List.708; + if List.705 then + let List.706 : {List I64, I64} = CallByName List.67 List.129 List.130 List.131; + ret List.706; + else + let List.704 : {List I64, I64} = Struct {List.129, List.131}; + ret List.704; + +procedure List.66 (#Attr.2, #Attr.3): + let List.717 : I64 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.717; + procedure List.67 (#Attr.2, #Attr.3, #Attr.4): - let List.699 : {List I64, I64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; - ret List.699; + let List.707 : {List I64, I64} = lowlevel ListReplaceUnsafe #Attr.2 #Attr.3 #Attr.4; + ret List.707; procedure Num.22 (#Attr.2, #Attr.3): let Num.291 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/compiler/test_mono/generated/unspecialized_lambda_set_unification_does_not_duplicate_identical_concrete_types.txt b/crates/compiler/test_mono/generated/unspecialized_lambda_set_unification_does_not_duplicate_identical_concrete_types.txt index be48ff974a..a4c0552f8c 100644 --- a/crates/compiler/test_mono/generated/unspecialized_lambda_set_unification_does_not_duplicate_identical_concrete_types.txt +++ b/crates/compiler/test_mono/generated/unspecialized_lambda_set_unification_does_not_duplicate_identical_concrete_types.txt @@ -29,58 +29,58 @@ procedure Encode.26 (Encode.107, Encode.108): let Encode.110 : List U8 = CallByName Encode.24 Encode.111 Encode.112 Encode.108; ret Encode.110; -procedure List.103 (#Derived_gen.9, #Derived_gen.10, #Derived_gen.11, #Derived_gen.12, #Derived_gen.13): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : Str = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : List U8 = CallByName Test.66 List.179 List.703 List.180; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.9, #Derived_gen.10, #Derived_gen.11, #Derived_gen.12, #Derived_gen.13): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : Str = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : List U8 = CallByName Test.66 List.180 List.711 List.181; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.9; - jump List.697 #Derived_gen.9 #Derived_gen.10 #Derived_gen.11 #Derived_gen.12 #Derived_gen.13; + jump List.705 #Derived_gen.9 #Derived_gen.10 #Derived_gen.11 #Derived_gen.12 #Derived_gen.13; procedure List.13 (#Attr.2, #Attr.3): - let List.720 : List Str = lowlevel ListPrepend #Attr.2 #Attr.3; - ret List.720; + let List.728 : List Str = lowlevel ListPrepend #Attr.2 #Attr.3; + ret List.728; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : List U8 = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : List U8 = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; -procedure List.4 (List.139, List.140): - let List.716 : U64 = 1i64; - let List.715 : List U8 = CallByName List.70 List.139 List.716; - let List.714 : List U8 = CallByName List.71 List.715 List.140; - ret List.714; +procedure List.4 (List.140, List.141): + let List.724 : U64 = 1i64; + let List.723 : List U8 = CallByName List.70 List.140 List.724; + let List.722 : List U8 = CallByName List.71 List.723 List.141; + ret List.722; procedure List.6 (#Attr.2): - let List.719 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.719; + let List.727 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.727; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure List.70 (#Attr.2, #Attr.3): - let List.710 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.710; + let List.718 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.718; procedure List.71 (#Attr.2, #Attr.3): - let List.708 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.708; + let List.716 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.716; procedure List.8 (#Attr.2, #Attr.3): - let List.718 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.718; + let List.726 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.726; procedure Num.127 (#Attr.2): let Num.290 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/unspecialized_lambda_set_unification_keeps_all_concrete_types_without_unification_of_unifiable.txt b/crates/compiler/test_mono/generated/unspecialized_lambda_set_unification_keeps_all_concrete_types_without_unification_of_unifiable.txt index 415aab4d1d..6dc2000ff8 100644 --- a/crates/compiler/test_mono/generated/unspecialized_lambda_set_unification_keeps_all_concrete_types_without_unification_of_unifiable.txt +++ b/crates/compiler/test_mono/generated/unspecialized_lambda_set_unification_keeps_all_concrete_types_without_unification_of_unifiable.txt @@ -87,93 +87,93 @@ procedure Encode.26 (Encode.107, Encode.108): let Encode.110 : List U8 = CallByName Encode.24 Encode.111 Encode.112 Encode.108; ret Encode.110; -procedure List.103 (#Derived_gen.44, #Derived_gen.45, #Derived_gen.46, #Derived_gen.47, #Derived_gen.48): - joinpoint List.724 List.178 List.179 List.180 List.181 List.182: - let List.726 : Int1 = CallByName Num.22 List.181 List.182; - if List.726 then - let List.730 : Str = CallByName List.66 List.178 List.181; - inc List.730; - let List.183 : List U8 = CallByName Test.66 List.179 List.730 List.180; - let List.729 : U64 = 1i64; - let List.728 : U64 = CallByName Num.51 List.181 List.729; - jump List.724 List.178 List.183 List.180 List.728 List.182; +procedure List.104 (#Derived_gen.44, #Derived_gen.45, #Derived_gen.46, #Derived_gen.47, #Derived_gen.48): + joinpoint List.705 List.179 List.180 List.181 List.182 List.183: + let List.707 : Int1 = CallByName Num.22 List.182 List.183; + if List.707 then + let List.711 : [C {}, C {}, C Str] = CallByName List.66 List.179 List.182; + inc List.711; + let List.184 : List U8 = CallByName Test.66 List.180 List.711 List.181; + let List.710 : U64 = 1i64; + let List.709 : U64 = CallByName Num.51 List.182 List.710; + jump List.705 List.179 List.184 List.181 List.709 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.44; - jump List.724 #Derived_gen.44 #Derived_gen.45 #Derived_gen.46 #Derived_gen.47 #Derived_gen.48; + jump List.705 #Derived_gen.44 #Derived_gen.45 #Derived_gen.46 #Derived_gen.47 #Derived_gen.48; -procedure List.103 (#Derived_gen.49, #Derived_gen.50, #Derived_gen.51, #Derived_gen.52, #Derived_gen.53): - joinpoint List.697 List.178 List.179 List.180 List.181 List.182: - let List.699 : Int1 = CallByName Num.22 List.181 List.182; - if List.699 then - let List.703 : [C {}, C {}, C Str] = CallByName List.66 List.178 List.181; - inc List.703; - let List.183 : List U8 = CallByName Test.66 List.179 List.703 List.180; - let List.702 : U64 = 1i64; - let List.701 : U64 = CallByName Num.51 List.181 List.702; - jump List.697 List.178 List.183 List.180 List.701 List.182; +procedure List.104 (#Derived_gen.49, #Derived_gen.50, #Derived_gen.51, #Derived_gen.52, #Derived_gen.53): + joinpoint List.732 List.179 List.180 List.181 List.182 List.183: + let List.734 : Int1 = CallByName Num.22 List.182 List.183; + if List.734 then + let List.738 : Str = CallByName List.66 List.179 List.182; + inc List.738; + let List.184 : List U8 = CallByName Test.66 List.180 List.738 List.181; + let List.737 : U64 = 1i64; + let List.736 : U64 = CallByName Num.51 List.182 List.737; + jump List.732 List.179 List.184 List.181 List.736 List.183; else - dec List.178; - ret List.179; + dec List.179; + ret List.180; in inc #Derived_gen.49; - jump List.697 #Derived_gen.49 #Derived_gen.50 #Derived_gen.51 #Derived_gen.52 #Derived_gen.53; + jump List.732 #Derived_gen.49 #Derived_gen.50 #Derived_gen.51 #Derived_gen.52 #Derived_gen.53; procedure List.13 (#Attr.2, #Attr.3): - let List.720 : List [C {}, C {}, C Str] = lowlevel ListPrepend #Attr.2 #Attr.3; - ret List.720; + let List.728 : List [C {}, C {}, C Str] = lowlevel ListPrepend #Attr.2 #Attr.3; + ret List.728; procedure List.13 (#Attr.2, #Attr.3): - let List.748 : List Str = lowlevel ListPrepend #Attr.2 #Attr.3; - ret List.748; + let List.756 : List Str = lowlevel ListPrepend #Attr.2 #Attr.3; + ret List.756; -procedure List.18 (List.175, List.176, List.177): - let List.695 : U64 = 0i64; - let List.696 : U64 = CallByName List.6 List.175; - let List.694 : List U8 = CallByName List.103 List.175 List.176 List.177 List.695 List.696; - ret List.694; +procedure List.18 (List.176, List.177, List.178): + let List.703 : U64 = 0i64; + let List.704 : U64 = CallByName List.6 List.176; + let List.702 : List U8 = CallByName List.104 List.176 List.177 List.178 List.703 List.704; + ret List.702; -procedure List.18 (List.175, List.176, List.177): - let List.722 : U64 = 0i64; - let List.723 : U64 = CallByName List.6 List.175; - let List.721 : List U8 = CallByName List.103 List.175 List.176 List.177 List.722 List.723; - ret List.721; +procedure List.18 (List.176, List.177, List.178): + let List.730 : U64 = 0i64; + let List.731 : U64 = CallByName List.6 List.176; + let List.729 : List U8 = CallByName List.104 List.176 List.177 List.178 List.730 List.731; + ret List.729; -procedure List.4 (List.139, List.140): - let List.743 : U64 = 1i64; - let List.742 : List U8 = CallByName List.70 List.139 List.743; - let List.741 : List U8 = CallByName List.71 List.742 List.140; - ret List.741; +procedure List.4 (List.140, List.141): + let List.751 : U64 = 1i64; + let List.750 : List U8 = CallByName List.70 List.140 List.751; + let List.749 : List U8 = CallByName List.71 List.750 List.141; + ret List.749; procedure List.6 (#Attr.2): - let List.719 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.719; + let List.727 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.727; procedure List.6 (#Attr.2): - let List.746 : U64 = lowlevel ListLenU64 #Attr.2; - ret List.746; + let List.754 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.754; procedure List.66 (#Attr.2, #Attr.3): - let List.704 : [C {}, C {}, C Str] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.704; + let List.712 : [C {}, C {}, C Str] = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.712; procedure List.66 (#Attr.2, #Attr.3): - let List.731 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.731; + let List.739 : Str = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.739; procedure List.70 (#Attr.2, #Attr.3): - let List.737 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; - ret List.737; + let List.745 : List U8 = lowlevel ListReserve #Attr.2 #Attr.3; + ret List.745; procedure List.71 (#Attr.2, #Attr.3): - let List.735 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; - ret List.735; + let List.743 : List U8 = lowlevel ListAppendUnsafe #Attr.2 #Attr.3; + ret List.743; procedure List.8 (#Attr.2, #Attr.3): - let List.745 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; - ret List.745; + let List.753 : List U8 = lowlevel ListConcat #Attr.2 #Attr.3; + ret List.753; procedure Num.127 (#Attr.2): let Num.294 : U8 = lowlevel NumIntCast #Attr.2; diff --git a/crates/compiler/test_mono/generated/weakening_avoids_overspecialization.txt b/crates/compiler/test_mono/generated/weakening_avoids_overspecialization.txt index bf297d1d16..cdaaf230cb 100644 --- a/crates/compiler/test_mono/generated/weakening_avoids_overspecialization.txt +++ b/crates/compiler/test_mono/generated/weakening_avoids_overspecialization.txt @@ -2,81 +2,81 @@ procedure Bool.9 (#Attr.2, #Attr.3): let Bool.21 : Int1 = lowlevel Eq #Attr.2 #Attr.3; ret Bool.21; -procedure List.119 (List.582, List.583, List.584): - let List.712 : U64 = 0i64; - let List.713 : U64 = CallByName List.6 List.582; - let List.711 : [C U64, C U64] = CallByName List.80 List.582 List.583 List.584 List.712 List.713; - ret List.711; +procedure List.120 (List.590, List.591, List.592): + let List.720 : U64 = 0i64; + let List.721 : U64 = CallByName List.6 List.590; + let List.719 : [C U64, C U64] = CallByName List.80 List.590 List.591 List.592 List.720 List.721; + ret List.719; -procedure List.26 (List.216, List.217, List.218): - let List.705 : [C U64, C U64] = CallByName List.119 List.216 List.217 List.218; - let List.708 : U8 = 1i64; - let List.709 : U8 = GetTagId List.705; - let List.710 : Int1 = lowlevel Eq List.708 List.709; - if List.710 then - let List.219 : U64 = UnionAtIndex (Id 1) (Index 0) List.705; - ret List.219; - else - let List.220 : U64 = UnionAtIndex (Id 0) (Index 0) List.705; +procedure List.26 (List.217, List.218, List.219): + let List.713 : [C U64, C U64] = CallByName List.120 List.217 List.218 List.219; + let List.716 : U8 = 1i64; + let List.717 : U8 = GetTagId List.713; + let List.718 : Int1 = lowlevel Eq List.716 List.717; + if List.718 then + let List.220 : U64 = UnionAtIndex (Id 1) (Index 0) List.713; ret List.220; + else + let List.221 : U64 = UnionAtIndex (Id 0) (Index 0) List.713; + ret List.221; -procedure List.38 (List.413, List.414): - let List.704 : U64 = CallByName List.6 List.413; - let List.415 : U64 = CallByName Num.77 List.704 List.414; - let List.694 : List U8 = CallByName List.43 List.413 List.415; - ret List.694; +procedure List.38 (List.414, List.415): + let List.712 : U64 = CallByName List.6 List.414; + let List.416 : U64 = CallByName Num.77 List.712 List.415; + let List.702 : List U8 = CallByName List.43 List.414 List.416; + ret List.702; -procedure List.43 (List.411, List.412): - let List.702 : U64 = CallByName List.6 List.411; - let List.701 : U64 = CallByName Num.77 List.702 List.412; - let List.696 : {U64, U64} = Struct {List.412, List.701}; - let List.695 : List U8 = CallByName List.49 List.411 List.696; - ret List.695; - -procedure List.49 (List.489, List.490): - let List.698 : U64 = StructAtIndex 1 List.490; - let List.699 : U64 = StructAtIndex 0 List.490; - let List.697 : List U8 = CallByName List.72 List.489 List.698 List.699; - ret List.697; - -procedure List.6 (#Attr.2): - let List.703 : U64 = lowlevel ListLenU64 #Attr.2; +procedure List.43 (List.412, List.413): + let List.710 : U64 = CallByName List.6 List.412; + let List.709 : U64 = CallByName Num.77 List.710 List.413; + let List.704 : {U64, U64} = Struct {List.413, List.709}; + let List.703 : List U8 = CallByName List.49 List.412 List.704; ret List.703; +procedure List.49 (List.497, List.498): + let List.706 : U64 = StructAtIndex 1 List.498; + let List.707 : U64 = StructAtIndex 0 List.498; + let List.705 : List U8 = CallByName List.72 List.497 List.706 List.707; + ret List.705; + +procedure List.6 (#Attr.2): + let List.711 : U64 = lowlevel ListLenU64 #Attr.2; + ret List.711; + procedure List.66 (#Attr.2, #Attr.3): - let List.726 : U8 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; - ret List.726; + let List.734 : U8 = lowlevel ListGetUnsafe #Attr.2 #Attr.3; + ret List.734; procedure List.72 (#Attr.2, #Attr.3, #Attr.4): - let List.700 : List U8 = lowlevel ListSublist #Attr.2 #Attr.3 #Attr.4; - ret List.700; + let List.708 : List U8 = lowlevel ListSublist #Attr.2 #Attr.3 #Attr.4; + ret List.708; procedure List.80 (Bool.22, Bool.23, Bool.24, Bool.25, Bool.26): - joinpoint List.714 List.585 List.586 List.587 List.588 List.589: - let List.716 : Int1 = CallByName Num.22 List.588 List.589; - if List.716 then - let List.725 : U8 = CallByName List.66 List.585 List.588; - let List.717 : [C U64, C U64] = CallByName Test.3 List.586 List.725; - let List.722 : U8 = 1i64; - let List.723 : U8 = GetTagId List.717; - let List.724 : Int1 = lowlevel Eq List.722 List.723; - if List.724 then - let List.590 : U64 = UnionAtIndex (Id 1) (Index 0) List.717; - let List.720 : U64 = 1i64; - let List.719 : U64 = CallByName Num.51 List.588 List.720; - jump List.714 List.585 List.590 List.587 List.719 List.589; + joinpoint List.722 List.593 List.594 List.595 List.596 List.597: + let List.724 : Int1 = CallByName Num.22 List.596 List.597; + if List.724 then + let List.733 : U8 = CallByName List.66 List.593 List.596; + let List.725 : [C U64, C U64] = CallByName Test.3 List.594 List.733; + let List.730 : U8 = 1i64; + let List.731 : U8 = GetTagId List.725; + let List.732 : Int1 = lowlevel Eq List.730 List.731; + if List.732 then + let List.598 : U64 = UnionAtIndex (Id 1) (Index 0) List.725; + let List.728 : U64 = 1i64; + let List.727 : U64 = CallByName Num.51 List.596 List.728; + jump List.722 List.593 List.598 List.595 List.727 List.597; else - dec List.585; - let List.591 : U64 = UnionAtIndex (Id 0) (Index 0) List.717; - let List.721 : [C U64, C U64] = TagId(0) List.591; - ret List.721; + dec List.593; + let List.599 : U64 = UnionAtIndex (Id 0) (Index 0) List.725; + let List.729 : [C U64, C U64] = TagId(0) List.599; + ret List.729; else - dec List.585; - let List.715 : [C U64, C U64] = TagId(1) List.586; - ret List.715; + dec List.593; + let List.723 : [C U64, C U64] = TagId(1) List.594; + ret List.723; in inc Bool.22; - jump List.714 Bool.22 Bool.23 Bool.24 Bool.25 Bool.26; + jump List.722 Bool.22 Bool.23 Bool.24 Bool.25 Bool.26; procedure Num.22 (#Attr.2, #Attr.3): let Num.292 : Int1 = lowlevel NumLt #Attr.2 #Attr.3; diff --git a/crates/error_macros/src/lib.rs b/crates/error_macros/src/lib.rs index 3f743e6ff4..5578fd602d 100644 --- a/crates/error_macros/src/lib.rs +++ b/crates/error_macros/src/lib.rs @@ -111,7 +111,8 @@ pub fn error_and_exit(args: fmt::Arguments) -> ! { pub const INTERNAL_ERROR_MESSAGE: &str = concat!( "An internal compiler expectation was broken.\n", "This is definitely a compiler bug.\n", - "Please file an issue here: \n", + "Note: this compiler is deprecated, we are unlikely to prioritize an issue with it.\n", + "Our new compiler is coming along nicely, it will be announced at when it is ready to completely replace this one.\n", ); /// `internal_error!` should be used whenever a compiler invariant is broken. diff --git a/langref/README.md b/langref/README.md new file mode 100644 index 0000000000..9d310c5bed --- /dev/null +++ b/langref/README.md @@ -0,0 +1,102 @@ +**NOTE:** This is heavily WIP! This line will be removed when the langref is considered complete, and at that point, missing sections and outdated information will be considered bugs. If you'd like to contribute to this (other than minor corrections of obvious mistakes), **definitely** mention it in the [langref Zulip channel](https://roc.zulipchat.com/#narrow/channel/316715-contributing/topic/language.20reference/with/564909757) before getting started on anything! + +# Language Reference + +This is Roc's language reference (or _langref_ for short). + +If you're looking for a beginner tutoral, check out [roc-lang.org/tutorial](https://roc-lang.org/tutorial) instead; the langref is a detailed reference intended for experienced Roc programmers. + +## Outline + +- [Expressions](expressions) + - [Values](expressions#values) + - [Reference Counting](expressions#reference-counting) + - [Reference Cycles](expressions#reference-cycles) + - [Opportunistic Mutation](expressions#opportunistic-mutation) + - [Block Expressions](expressions#block-expressions) +- [Statements](statements) + - [`=` (assignment)](statements#assignment) + - [`import`](statements#import) + - [`expect`](statements#expect) + - [`return`](statements#return) + - [`break`](statements#break) + - [`continue`](statements#continue) + - [Block Statements](statements#block-statements) +- [Conditionals](conditionals) + - [`if`](conditionals#if) + - [`else`](conditionals#else) + - [`else if`](conditionals#else-if) + - [`and`](conditionals#and) + - [`or`](conditionals#or) + - [`match`](conditionals#match) +- [Pattern Matching](pattern-matching) + - [`match`](pattern-matching#match) + - [Branch Alternatives](pattern-matching#alternatives) + - [`if` Guards on Branches](pattern-matching#if-guards-on-branches) + - [Exhaustiveness](pattern-matching#exhaustiveness) + - [Catch-all Patterns (`_`)](pattern-matching#catch-all-patterns-underscore) + - [Destructuring](pattern-matching#destructuring) + - [Destructuring Assignments (with `=`)](pattern-matching#destructuring-assignments) +- [Functions](functions) + - [Pure Functions](functions#pure-functions) + - [Effectful Functions](functions#effectful-functions) + - [`!` suffix](functions#!-suffix) + - [Recursive Functions](functions#recursive-functions) + - [Self-recursive Functions](functions#self-recursive-functions) + - [Mutually Recursive Functions](functions#mutually-recursive-functions) + - [Tail Calls](functions#tail-calls) + - [Self-tail Calls](functions#self-tail-calls) + - [Tail Call Optimization](functions#tail-call-optimization) + - [Modulo Cons](functions#modulo-cons) +- [Naming](naming) + - [Shadowing](naming#shadowing) + - [Constants](naming#constants) + - [Variables (with `var`)](naming#variables) + - [`var` keyword](naming#var-keyword) + - [`$` suffix](naming#$-suffix) + - [Type Variables](naming#type-variables) + - [Type Aliases](naming#type-aliases) + - [Parameterized Type Aliases](naming#parameterized-type-aliases) + - [Module Names](naming#module-names) + - [`as`](naming#as) +- [Modules](modules) + - [Type Modules](modules#type-modules) + - [Imports](modules#imports) + - [Package Modules](modules#package-modules) + - [Platform Modules](modules#platform-modules) + - [Application Modules](modules#application-modules) +- [Types](types) + - [Type Annotations](types#type-annotations) + - [Where Clauses](types#where-clauses) + - [Nominal Types](types#nominal-types) + - [Opaque Nominal Types](types#opaque-nominal-types) + - [Structural Types](types#structural-types) + - [Type Aliases](types#type-aliases) +- [Static Dispatch](static-dispatch) + - [Methods](static-dispatch#methods) + - [Where Clauses](static-dispatch#where-clauses) + - [Aliases](static-dispatch#aliases) +- [Operators](operators) + - [Desugaring](operators#desugaring) + - [Binary Infix Operations](operators#binary-infix-operations) + - [And](operators#and) + - [Or](operators#or) + - [Arithmetic Operators](operators#arithmetic-operators) + - [Comparison Operators](operators#comparison-operators) + - [Unary Prefix Operators](operators#unary-prefix-operators) + - [`-` (`.negate()`)](operators#-) + - [`!` (`.not()`)](operators#-1) + - [Unary Postfix Operators](operators#unary-postfix-operators) + - [`?` (unwrap if `Ok`; early `return` if `Err`)](operators#-2) + - [`[…]` (subscript operator)](operators#-subscript-operator) +- [Collections](collections) + - [Structural Records](collections#structural-records) + - [Nominal Records](collections#nominal-records) + - [Tuples](collections#tuples) + - [Structural Tag Unions](collections#structural-tag-unions) + - [Nominal Tag Unions](collections#nominal-tag-unions) + - [Builtin Collections](collections#builtin-collections) +- [Loops](loops) + - [`for` Loops](loops#for) + - [`while` Loop](loops#while) + - [Infinite Loops](loops#infinite-loops) diff --git a/langref/collections.md b/langref/collections.md new file mode 100644 index 0000000000..8e4fcce4a2 --- /dev/null +++ b/langref/collections.md @@ -0,0 +1,13 @@ +# Collections + +## Structural Records + +## Nominal Records + +## Tuples + +## Structural Tag Unions + +## Nominal Tag Unions + +## Builtin Collections diff --git a/langref/conditionals.md b/langref/conditionals.md new file mode 100644 index 0000000000..cf24a9f4d1 --- /dev/null +++ b/langref/conditionals.md @@ -0,0 +1,13 @@ +# Conditionals + +## `if` + +## `else` + +### `else if` + +## `and` + +## `or` + +## `match` diff --git a/langref/expressions.md b/langref/expressions.md new file mode 100644 index 0000000000..6a45eeda1c --- /dev/null +++ b/langref/expressions.md @@ -0,0 +1,140 @@ +# Expressions + +An expression is something that evaluates to a [value](#values). + +You can wrap expressions in parentheses without changing what they do. Non-expressions +can't be wrapped in parentheses without causing an error. Some examples: + +- `x` is a valid expression. It evaluates to a value. `(x)` is valid. +- `foo(1)` is a valid expression. It evaluates to a value. `(foo(1))` is valid. +- `1` is a valid expression. It's already a value `(1)` is valid. +- `import Foo` is a [statement](statements) not an expression. `(import Foo)` is invalid. +- `# Something` is a [comment](comments), not an expression. `(# Something)` is invalid. +- `package […]` is a [module heaader](modules#headers), not an expression. `(package […])` is invalid. + +Another way to think of an expression is that you can always assign it to a name—so, you +can always put it after an `=` sign. + +## [Types of Expressions](#literal-expressions) {#literal-expression} + +Here are all the different types of expressions in Roc: + +- String literals, e.g. `"foo"` or `"Hello, ${name}!"` +- Number literals, e.g. `1` or `2.34` or `0.123e4` +- List literals, e.g. `[1, 2]` or `[]` or `["foo"]` +- Record literals, e.g. `{ x: 1, y: 2 }` or `{}` or `{ x, y, ..other_record }` +- Tag literals, e.g. `Foo` or `Foo(bar)` +- Tuple literals, e.g. `(a, b, "foo")` +- Function literals (aka "lambdas"), e.g. `|a, b| a + b` or `|| c + d` +- Lookups, e.g. `blah` or `(blah)` or `$blah` or `($blah)` or `blah!` or `(blah!)` +- Calls, e.g. `blah(arg)` or `foo.bar(baz)` +- Operator applications, e.g. `a + b` or `!x`, which [desugar to calls](operators#desugaring) +- [Block expressions](#block-expressions), e.g. `{ foo() }` + +There are no other types of expressions in the language. + +## [Values](#values) {#values} + +A Roc value is a semantically immutable piece of data. + +### [Value Identity](#value-identity) {#value-identity} + +Since values take up memory at runtime, each value has a [memory address](https://en.wikipedia.org/wiki/Memory_address). +Roc treats memory addresses as behind-the-scenes implementation details that should not affect +program behavior, and by design exposes no language-level way to access or compare addresses. + +This implies that Roc has no concept of [value identity](https://en.wikipedia.org/wiki/Identity_(object-oriented_programming)), reference equality (also known as [physical equality](https://ocaml.org/manual/5.4/api/Repr.html#VALphys_equal)), or [pointers](https://en.wikipedia.org/wiki/Pointer_(computer_programming)), all of which are semantic concepts based on memory addresses. + +> Note that platform authors can choose to implement features based on memory addresses, +> since platforms have access to lower-level languages which can naturally see the addresses +> of any Roc value the platform receives. This means it's up to a platform author to decide +> whether it's a good idea for users of their platform to start needing to think about memory +> addresses, when the rest of the language is designed to keep them behind the scenes. + +### [Reference Counting](#reference-counting) {#reference-counting} + +Heap-allocated Roc values are automatically [reference-counted](https://en.wikipedia.org/wiki/Reference_counting) ([atomically](https://en.wikipedia.org/wiki/Linearizability#Primitive_atomic_instructions), for thread-safety). + +Heap-allocated values include as strings, lists, boxes, and recursive tag unions. Numbers, +records, tuples, and non-recursive tag unions are stack-allocated, and so are not reference counted. + +### [Reference Cycles](#reference-cycles) {#reference-cycles} + +Other languages support [reference cycles](https://en.wikipedia.org/wiki/Reference_counting#Dealing_with_reference_cycles), which create problems for reference counting systems. Solutions to these problems include runtime tracing garbage collectors for cycles, or a concept of [weak references](https://en.wikipedia.org/wiki/Weak_reference). By design, Roc has no way to express reference cycles, so none of these solutions are necessary. + +### [Opportunistic Mutation](#opportunistic-mutation) {#opportunistic-mutation} + +Roc's compiler does _opportunistic mutation_ using the [Perceus](https://www.microsoft.com/en-us/research/wp-content/uploads/2020/11/perceus-tr-v4.pdf) "functional-but-in-place" reference counting system. The way this works is: + +- Builtin operations on reference-counted values will update them in place when their reference counts are 1 +- When their reference counts are greater than 1, they will be shallowly cloned first, and then the clone will be updated and returned. + +For example, when [`List.set`](../builtins/List#set) is passed a unique list (reference count is 1), then that list will have the given element replaced. When it's given a shared list (reference count is not 1), it will first shallowly clone the list, and then replace the given element in the clone. Either way, the modified list will be returned—regardless of whether the clone or the original was the one modified. + +## [Block Expressions](#block-expressions) {#block-expressions} + +A _block expression_ is an expression with some optional [statements](statements) +before the expression. It has its own scope, so anything assigned in it can't be accessed +outsdie the block. The entire block evaluates to the expression at its end. + +The statements are optional, so `{ x }` is a valid block expression. This is useful +stylistically in situations like conditional branches: + +```roc +x = if foo { + … +} else { + x +} +``` + +> Note that `{ x, y }` is a [record](records) with two fields (it's syntax sugar for `{ x: x, y: y }`), +> but `{ x }` is always a block expression. That's because it's much more useful to have `{ x }` +> be a block expression, for situations like `else { x }`, than syntax sugar for a single-field +> record like `{ x: x }`. Single-field records are much less common than blocks in +> conditional branches. + +## [Evaluation](#evaluation) {#evaluation} + +Evaluation is the process of an expression becoming a value. + +Like most programming languages, Roc uses [strict evaluation](https://en.wikipedia.org/wiki/Strict_programming_language) and does not support [lazy evaluation](https://en.wikipedia.org/wiki/Strict_programming_language) like some non-strict languages do (such as [Haskell](https://www.haskell.org)). + +Expressions that are already values (such as `4`, `"foo"`, etc.) evaluate to themselves. + +More complex expressions, such as function calls or [block expressions](#block-expressions) +may require multiple steps to evaluate to a value. + +### [Side Effects during evaluation](#side-effects-during-evaluation) {#side-effects-during-evaluation} + +Normally, only evaluating [effectful functions](functions#effectful-functions) can +cause [side effects](functions#side-effects), but evaluating any other type of expression cannot. + +One exception to this rule is [`dbg` statements](statements#dbg), which perform the side effect of +logging a value for debugging purposes. This is intended to be an interface for debugging, much +like a [step debugger](https://en.wikipedia.org/wiki/Debugger), not part of a program's semantics, +and so the side effect is allowed outside effectful functions (just like step debugging works on +any expression, not just calling effectful functions). + +The only other exception to the rule is [`expect` statements](statements#expect), + +> Note that platform authors are in charge of what happens when memory gets allocated and +> deallocated, and can therefore decide to perform side effects during memory allocation +> and deallocation. This can easily cause surprising behavior for Roc applicationa authors, +> and should not be relied upon because Roc's optimizer assumes memory allocation and deallocation +> has no observable effect on the program (unless allocation fails), which means these side +> effects may be optimized away differently between patch releases of the compiler. + +### [Compile-Time Evaluation](#compile-time-evaluation) {#compile-time-evaluation} + +When possible, Roc's compiler will evaluate expressions at compile-time instead of at runtime. + +This is only possible for expressions that depend only on values known at compile-time. The +following are not known at compile time: + +- Values returned from effectful function calls (which can vary based on runtime state) +- Values provided by the platform + +If a [`crash`](statements#crash) is encountered during compile-time evaluation, +it will be reported at compile time just like any other compilation errors (such as +syntax errors, naming errors, and type mismatches). diff --git a/langref/functions.md b/langref/functions.md new file mode 100644 index 0000000000..18e23152e8 --- /dev/null +++ b/langref/functions.md @@ -0,0 +1,23 @@ +# Functions + +## Pure Functions + +### Side Effects + +## Effectful Functions + +### `!` suffix + +## Recursive Functions + +### Self-recursive Functions + +### Mutually Recursive Functions + +## Tail Calls + +### Self-tail Calls + +### Tail Call Optimization + +#### Modulo Cons diff --git a/langref/loops.md b/langref/loops.md new file mode 100644 index 0000000000..c4ace2bef7 --- /dev/null +++ b/langref/loops.md @@ -0,0 +1,7 @@ +# Loops + +## `for` Loops + +## `while` Loop + +## Infinite Loops diff --git a/langref/modules.md b/langref/modules.md new file mode 100644 index 0000000000..e5861f7abb --- /dev/null +++ b/langref/modules.md @@ -0,0 +1,15 @@ +# Modules + +## Type Modules + +## Imports + +## Module Headers + +## Package Modules + +## Platform Modules + +## Application Modules + +### Headerless Application Modules diff --git a/langref/naming.md b/langref/naming.md new file mode 100644 index 0000000000..244ea2b543 --- /dev/null +++ b/langref/naming.md @@ -0,0 +1,21 @@ +# Naming + +## Shadowing + +## Constants + +## Variables (with `var`) + +### `var` keyword + +### `$` suffix + +## Type Variables + +## Type Aliases + +### Parameterized Type Aliases + +## Module Names + +## `as` diff --git a/langref/operators.md b/langref/operators.md new file mode 100644 index 0000000000..9320b040e6 --- /dev/null +++ b/langref/operators.md @@ -0,0 +1,25 @@ +# Operators + +## Desugaring + +## Binary Infix Operations + +### And + +### Or + +### Arithmetic Operators + +### Comparison Operators + +## Unary Prefix Operators + +### `-` (`.negate()`) + +### `!` (`.not()`) + +## Unary Postfix Operators + +### `?` (unwrap if `Ok`; early `return` if `Err`) + +### `[…]` (subscript operator) diff --git a/langref/pattern-matching.md b/langref/pattern-matching.md new file mode 100644 index 0000000000..6709d2791b --- /dev/null +++ b/langref/pattern-matching.md @@ -0,0 +1,15 @@ +# Pattern Matching + +## `match` + +### Branch Alternatives + +### `if` Guards on Branches + +## Exhaustiveness + +### Catch-all Patterns (`_`) + +## Destructuring + +### Destructuring Assignments (with `=`) diff --git a/langref/statements.md b/langref/statements.md new file mode 100644 index 0000000000..8a036e6452 --- /dev/null +++ b/langref/statements.md @@ -0,0 +1,149 @@ +# Statements + +Statements are run as soon as they are encountered at runtime. +They do not [evaluate](expressions#evaluation) to a [value](expressions#value). + +## [`=` (assignment)](#assignment) {#assignment} + +An _assignment statement_ gives a name to a [value](expressions#value) inside the current scope. + +### [Assignment Order](#assignment-order) {#assignment-order} + +Assignments inside expressions can only reference names that were assigned earlier in scope. +For example, this would be an error: + +```roc +foo({ + y = z + 1 + z = 5 + + z + 1 +}) +``` + +However, at the top level of a module, assignments can reference each other +regardless of declaration order: + +```roc +x = y + 1 +y = 5 +``` + +### [Assignment Cycles](#assignment-cycles) {#assignment-cycles} + +Top-level assignments can only mutually reference each other if they are all assigning to functions. +This gives an error at compile time: + +```roc +x = y + 1 +y = x + 1 +``` + +(If it did not give an error at compile time, it would either crash or loop infinitely at runtime.) + +In contrast, this gives no error because all the assignments in the cycle are assigning to functions: + +```roc +x = |arg| if arg >= 1 { y(arg + 1) } else { 0 } +y = |arg| if arg <= 9 { x(arg + 1) } else { 0 } +``` + +### [Reassignment](#reassignment) {#reassignment} + +Reassigning to an existing name is only allowed when the name was declared with +[`var`](pattern-matching#var). This is allowed: + +```roc +var $foo = 0 +$foo = 1 +``` + +However, this gives a [shadowing](naming#shadowing) error: + +```roc +foo = 0 +foo = 1 +``` + +## [`import`](#import) {#import} + +The `import` statement imports a [type](types) into scope from a [type module](modules#type-modules). + +### [`import` with `exposing`](#import-exposing) {#import-exposing} + +### [Renaming Imports with `as`](#renaming-imports) {#renaming-imports} + +### [Importing non-Roc files](#importing-non-roc-files) {#importing-non-roc-files} + +## [`dbg`](#dbg) {#dbg} + +## [`expect`](#expect) {#expect} + +## [`return`](#return) {#return} + +The `return` statement immediately exits a function, returning the given value. + +```roc +my_func = |arg| { + if arg == 0 { + return 0 + + # This line will never be reached. + } + + arg - 1 +} +``` + +## [`break`](#break) {#break} + +(This has not been implemented yet. It will exit a `for` or `while` loop.) + +## [`continue`](#continue) {#continue} + +(This has not been implemented yet. It will continue to the next iteration of a `for` or `while` loop.) + +## [`crash`](#crash) {#crash} + +A `crash` statement crashes the running application. All code following the `crash` +becomes unreachable and will not be executed. + +```roc +if some_condition { + crash "There is no way this program could possibly continue." +} + +# This line will never be reached if `some_condition` was `True` +``` + +What happens after a `crash` is determined by the platform. Some may gracefully recover +and have some way of continuing the process, but others may terminate the process immediately. + +## [Block Statements](#block-statements) {#block-statements} + +A _block statement_ is a group of statements which has its own scope, so +anything [assigned](#assignment) in it can't be accessed outsdie the block. + +It's different from a [block expression](expressions#block-expressions) in that +a block statement does not have an expression at the end. A common block +statement is one that does an early `return` in a conditional branch: + +```roc +if foo { + … +} else { + bar = … + + return bar +} +``` + +Having a single statement in a block expression is allowed: + +```roc +if foo { + … +} else { + return bar +} +``` diff --git a/langref/static-dispatch.md b/langref/static-dispatch.md new file mode 100644 index 0000000000..4507a070dd --- /dev/null +++ b/langref/static-dispatch.md @@ -0,0 +1,7 @@ +# Static Dispatch + +## Methods + +## Where Clauses + +## Aliases diff --git a/langref/types.md b/langref/types.md new file mode 100644 index 0000000000..a6dc552fc5 --- /dev/null +++ b/langref/types.md @@ -0,0 +1,13 @@ +# Types + +## Type Annotations + +### Where Clauses + +## Nominal Types + +### Opaque Nominal Types + +## Structural Types + +## Type Aliases diff --git a/src/PROFILING/README.md b/src/PROFILING/README.md index b1bc133277..1f640379ee 100644 --- a/src/PROFILING/README.md +++ b/src/PROFILING/README.md @@ -34,7 +34,7 @@ They also lack interactivity and many low level details. Flamegraphs are essentially a still frame view into an apps performance. The tool is simple to run. -Using it with roc can be as simple as `flamegraph --root -- ./zig-out/bin/roc format /tmp/app.roc`. +Using it with roc can be as simple as `flamegraph --root -- ./zig-out/bin/roc fmt /tmp/app.roc`. > Note: `--root` is not always needed, but it generally gives better results. @@ -51,7 +51,7 @@ Samply is a sampling profiler that that uses the [Firefox profiler](https://prof I find it to be a richer and nicer default than simple flamegraphs, but it is also more complex. Not only do you get a flamegraph, but you get callstacks, can collapse recursive nodes, and are able to view source lines and assembly hit by the sampler. -Using samply is also super simply, just `samply record -- ./zig-out/bin/roc format /tmp/new.roc`. +Using samply is also super simply, just `samply record -- ./zig-out/bin/roc fmt /tmp/new.roc`. Here is an example output after searching for `memmove`. We can see in the top graph, all the specific times during execution that memmove was using the cpu. @@ -79,11 +79,11 @@ Often times, it is worth profiling once with `-Dtracy-callstack=false` to have m Now that I have the compiler built with tracy, I can launch the tracy server on my mac machine to record the result (you can also run it on the same machine if wanted). Run `tracy-profiler`, input the correct ip address, and press connect. -Then run the instrumented version of zig: `./zig-out/bin/roc format /tmp/new.roc`. +Then run the instrumented version of zig: `./zig-out/bin/roc fmt /tmp/new.roc`. Also, run with the root user to capture more information. In this case, I ran: ``` -sudo ./zig-out/bin/roc format /tmp/new.roc +sudo ./zig-out/bin/roc fmt /tmp/new.roc ``` After that, welcome to a wealth of information: diff --git a/src/PROFILING/bench_repeated_check.roc b/src/PROFILING/bench_repeated_check.roc index 6e575f222b..7e1f3dda04 100644 --- a/src/PROFILING/bench_repeated_check.roc +++ b/src/PROFILING/bench_repeated_check.roc @@ -1,23488 +1,7 @@ -## -## !! Do not alter this file unless necessary !! -## -## Compiler phase benchmarks use this file, see `src/PROFILING/exec_bench.roc`. -## If the file changes, the benchmarks can't track performance over time. -## +# Minimal test file - original benchmark temporarily disabled +# See bench_repeated_check_ORIGINAL.roc for the full version +# TODO: Re-enable once `!=` (which desugars to is_eq().not()) is fully working -module [ - x, - y, - z, - my_str, - binops, - add_one, - map_add_one, - test_func, - multiply, - num, - frac, - str, - empty_list, - mixed, - x_2, - y_2, - z_2, - my_str_2, - binops_2, - add_one_2, - map_add_one_2, - test_func_2, - multiply_2, - num_2, - frac_2, - str_2, - empty_list_2, - mixed_2, - x_3, - y_3, - z_3, - my_str_3, - binops_3, - add_one_3, - map_add_one_3, - test_func_3, - multiply_3, - num_3, - frac_3, - str_3, - empty_list_3, - mixed_3, - x_4, - y_4, - z_4, - my_str_4, - binops_4, - add_one_4, - map_add_one_4, - test_func_4, - multiply_4, - num_4, - frac_4, - str_4, - empty_list_4, - mixed_4, - x_5, - y_5, - z_5, - my_str_5, - binops_5, - add_one_5, - map_add_one_5, - test_func_5, - multiply_5, - num_5, - frac_5, - str_5, - empty_list_5, - mixed_5, - x_6, - y_6, - z_6, - my_str_6, - binops_6, - add_one_6, - map_add_one_6, - test_func_6, - multiply_6, - num_6, - frac_6, - str_6, - empty_list_6, - mixed_6, - x_7, - y_7, - z_7, - my_str_7, - binops_7, - add_one_7, - map_add_one_7, - test_func_7, - multiply_7, - num_7, - frac_7, - str_7, - empty_list_7, - mixed_7, - x_8, - y_8, - z_8, - my_str_8, - binops_8, - add_one_8, - map_add_one_8, - test_func_8, - multiply_8, - num_8, - frac_8, - str_8, - empty_list_8, - mixed_8, - x_9, - y_9, - z_9, - my_str_9, - binops_9, - add_one_9, - map_add_one_9, - test_func_9, - multiply_9, - num_9, - frac_9, - str_9, - empty_list_9, - mixed_9, - x_10, - y_10, - z_10, - my_str_10, - binops_10, - add_one_10, - map_add_one_10, - test_func_10, - multiply_10, - num_10, - frac_10, - str_10, - empty_list_10, - mixed_10, - x_11, - y_11, - z_11, - my_str_11, - binops_11, - add_one_11, - map_add_one_11, - test_func_11, - multiply_11, - num_11, - frac_11, - str_11, - empty_list_11, - mixed_11, - x_12, - y_12, - z_12, - my_str_12, - binops_12, - add_one_12, - map_add_one_12, - test_func_12, - multiply_12, - num_12, - frac_12, - str_12, - empty_list_12, - mixed_12, - x_13, - y_13, - z_13, - my_str_13, - binops_13, - add_one_13, - map_add_one_13, - test_func_13, - multiply_13, - num_13, - frac_13, - str_13, - empty_list_13, - mixed_13, - x_14, - y_14, - z_14, - my_str_14, - binops_14, - add_one_14, - map_add_one_14, - test_func_14, - multiply_14, - num_14, - frac_14, - str_14, - empty_list_14, - mixed_14, - x_15, - y_15, - z_15, - my_str_15, - binops_15, - add_one_15, - map_add_one_15, - test_func_15, - multiply_15, - num_15, - frac_15, - str_15, - empty_list_15, - mixed_15, - x_16, - y_16, - z_16, - my_str_16, - binops_16, - add_one_16, - map_add_one_16, - test_func_16, - multiply_16, - num_16, - frac_16, - str_16, - empty_list_16, - mixed_16, - x_17, - y_17, - z_17, - my_str_17, - binops_17, - add_one_17, - map_add_one_17, - test_func_17, - multiply_17, - num_17, - frac_17, - str_17, - empty_list_17, - mixed_17, - x_18, - y_18, - z_18, - my_str_18, - binops_18, - add_one_18, - map_add_one_18, - test_func_18, - multiply_18, - num_18, - frac_18, - str_18, - empty_list_18, - mixed_18, - x_19, - y_19, - z_19, - my_str_19, - binops_19, - add_one_19, - map_add_one_19, - test_func_19, - multiply_19, - num_19, - frac_19, - str_19, - empty_list_19, - mixed_19, - x_20, - y_20, - z_20, - my_str_20, - binops_20, - add_one_20, - map_add_one_20, - test_func_20, - multiply_20, - num_20, - frac_20, - str_20, - empty_list_20, - mixed_20, - x_21, - y_21, - z_21, - my_str_21, - binops_21, - add_one_21, - map_add_one_21, - test_func_21, - multiply_21, - num_21, - frac_21, - str_21, - empty_list_21, - mixed_21, - x_22, - y_22, - z_22, - my_str_22, - binops_22, - add_one_22, - map_add_one_22, - test_func_22, - multiply_22, - num_22, - frac_22, - str_22, - empty_list_22, - mixed_22, - x_23, - y_23, - z_23, - my_str_23, - binops_23, - add_one_23, - map_add_one_23, - test_func_23, - multiply_23, - num_23, - frac_23, - str_23, - empty_list_23, - mixed_23, - x_24, - y_24, - z_24, - my_str_24, - binops_24, - add_one_24, - map_add_one_24, - test_func_24, - multiply_24, - num_24, - frac_24, - str_24, - empty_list_24, - mixed_24, - x_25, - y_25, - z_25, - my_str_25, - binops_25, - add_one_25, - map_add_one_25, - test_func_25, - multiply_25, - num_25, - frac_25, - str_25, - empty_list_25, - mixed_25, - x_26, - y_26, - z_26, - my_str_26, - binops_26, - add_one_26, - map_add_one_26, - test_func_26, - multiply_26, - num_26, - frac_26, - str_26, - empty_list_26, - mixed_26, - x_27, - y_27, - z_27, - my_str_27, - binops_27, - add_one_27, - map_add_one_27, - test_func_27, - multiply_27, - num_27, - frac_27, - str_27, - empty_list_27, - mixed_27, - x_28, - y_28, - z_28, - my_str_28, - binops_28, - add_one_28, - map_add_one_28, - test_func_28, - multiply_28, - num_28, - frac_28, - str_28, - empty_list_28, - mixed_28, - x_29, - y_29, - z_29, - my_str_29, - binops_29, - add_one_29, - map_add_one_29, - test_func_29, - multiply_29, - num_29, - frac_29, - str_29, - empty_list_29, - mixed_29, - x_30, - y_30, - z_30, - my_str_30, - binops_30, - add_one_30, - map_add_one_30, - test_func_30, - multiply_30, - num_30, - frac_30, - str_30, - empty_list_30, - mixed_30, - x_31, - y_31, - z_31, - my_str_31, - binops_31, - add_one_31, - map_add_one_31, - test_func_31, - multiply_31, - num_31, - frac_31, - str_31, - empty_list_31, - mixed_31, - x_32, - y_32, - z_32, - my_str_32, - binops_32, - add_one_32, - map_add_one_32, - test_func_32, - multiply_32, - num_32, - frac_32, - str_32, - empty_list_32, - mixed_32, - x_33, - y_33, - z_33, - my_str_33, - binops_33, - add_one_33, - map_add_one_33, - test_func_33, - multiply_33, - num_33, - frac_33, - str_33, - empty_list_33, - mixed_33, - x_34, - y_34, - z_34, - my_str_34, - binops_34, - add_one_34, - map_add_one_34, - test_func_34, - multiply_34, - num_34, - frac_34, - str_34, - empty_list_34, - mixed_34, - x_35, - y_35, - z_35, - my_str_35, - binops_35, - add_one_35, - map_add_one_35, - test_func_35, - multiply_35, - num_35, - frac_35, - str_35, - empty_list_35, - mixed_35, - x_36, - y_36, - z_36, - my_str_36, - binops_36, - add_one_36, - map_add_one_36, - test_func_36, - multiply_36, - num_36, - frac_36, - str_36, - empty_list_36, - mixed_36, - x_37, - y_37, - z_37, - my_str_37, - binops_37, - add_one_37, - map_add_one_37, - test_func_37, - multiply_37, - num_37, - frac_37, - str_37, - empty_list_37, - mixed_37, - x_38, - y_38, - z_38, - my_str_38, - binops_38, - add_one_38, - map_add_one_38, - test_func_38, - multiply_38, - num_38, - frac_38, - str_38, - empty_list_38, - mixed_38, - x_39, - y_39, - z_39, - my_str_39, - binops_39, - add_one_39, - map_add_one_39, - test_func_39, - multiply_39, - num_39, - frac_39, - str_39, - empty_list_39, - mixed_39, - x_40, - y_40, - z_40, - my_str_40, - binops_40, - add_one_40, - map_add_one_40, - test_func_40, - multiply_40, - num_40, - frac_40, - str_40, - empty_list_40, - mixed_40, - x_41, - y_41, - z_41, - my_str_41, - binops_41, - add_one_41, - map_add_one_41, - test_func_41, - multiply_41, - num_41, - frac_41, - str_41, - empty_list_41, - mixed_41, - x_42, - y_42, - z_42, - my_str_42, - binops_42, - add_one_42, - map_add_one_42, - test_func_42, - multiply_42, - num_42, - frac_42, - str_42, - empty_list_42, - mixed_42, - x_43, - y_43, - z_43, - my_str_43, - binops_43, - add_one_43, - map_add_one_43, - test_func_43, - multiply_43, - num_43, - frac_43, - str_43, - empty_list_43, - mixed_43, - x_44, - y_44, - z_44, - my_str_44, - binops_44, - add_one_44, - map_add_one_44, - test_func_44, - multiply_44, - num_44, - frac_44, - str_44, - empty_list_44, - mixed_44, - x_45, - y_45, - z_45, - my_str_45, - binops_45, - add_one_45, - map_add_one_45, - test_func_45, - multiply_45, - num_45, - frac_45, - str_45, - empty_list_45, - mixed_45, - x_46, - y_46, - z_46, - my_str_46, - binops_46, - add_one_46, - map_add_one_46, - test_func_46, - multiply_46, - num_46, - frac_46, - str_46, - empty_list_46, - mixed_46, - x_47, - y_47, - z_47, - my_str_47, - binops_47, - add_one_47, - map_add_one_47, - test_func_47, - multiply_47, - num_47, - frac_47, - str_47, - empty_list_47, - mixed_47, - x_48, - y_48, - z_48, - my_str_48, - binops_48, - add_one_48, - map_add_one_48, - test_func_48, - multiply_48, - num_48, - frac_48, - str_48, - empty_list_48, - mixed_48, - x_49, - y_49, - z_49, - my_str_49, - binops_49, - add_one_49, - map_add_one_49, - test_func_49, - multiply_49, - num_49, - frac_49, - str_49, - empty_list_49, - mixed_49, - x_50, - y_50, - z_50, - my_str_50, - binops_50, - add_one_50, - map_add_one_50, - test_func_50, - multiply_50, - num_50, - frac_50, - str_50, - empty_list_50, - mixed_50, - x_51, - y_51, - z_51, - my_str_51, - binops_51, - add_one_51, - map_add_one_51, - test_func_51, - multiply_51, - num_51, - frac_51, - str_51, - empty_list_51, - mixed_51, - x_52, - y_52, - z_52, - my_str_52, - binops_52, - add_one_52, - map_add_one_52, - test_func_52, - multiply_52, - num_52, - frac_52, - str_52, - empty_list_52, - mixed_52, - x_53, - y_53, - z_53, - my_str_53, - binops_53, - add_one_53, - map_add_one_53, - test_func_53, - multiply_53, - num_53, - frac_53, - str_53, - empty_list_53, - mixed_53, - x_54, - y_54, - z_54, - my_str_54, - binops_54, - add_one_54, - map_add_one_54, - test_func_54, - multiply_54, - num_54, - frac_54, - str_54, - empty_list_54, - mixed_54, - x_55, - y_55, - z_55, - my_str_55, - binops_55, - add_one_55, - map_add_one_55, - test_func_55, - multiply_55, - num_55, - frac_55, - str_55, - empty_list_55, - mixed_55, - x_56, - y_56, - z_56, - my_str_56, - binops_56, - add_one_56, - map_add_one_56, - test_func_56, - multiply_56, - num_56, - frac_56, - str_56, - empty_list_56, - mixed_56, - x_57, - y_57, - z_57, - my_str_57, - binops_57, - add_one_57, - map_add_one_57, - test_func_57, - multiply_57, - num_57, - frac_57, - str_57, - empty_list_57, - mixed_57, - x_58, - y_58, - z_58, - my_str_58, - binops_58, - add_one_58, - map_add_one_58, - test_func_58, - multiply_58, - num_58, - frac_58, - str_58, - empty_list_58, - mixed_58, - x_59, - y_59, - z_59, - my_str_59, - binops_59, - add_one_59, - map_add_one_59, - test_func_59, - multiply_59, - num_59, - frac_59, - str_59, - empty_list_59, - mixed_59, - x_60, - y_60, - z_60, - my_str_60, - binops_60, - add_one_60, - map_add_one_60, - test_func_60, - multiply_60, - num_60, - frac_60, - str_60, - empty_list_60, - mixed_60, - x_61, - y_61, - z_61, - my_str_61, - binops_61, - add_one_61, - map_add_one_61, - test_func_61, - multiply_61, - num_61, - frac_61, - str_61, - empty_list_61, - mixed_61, - x_62, - y_62, - z_62, - my_str_62, - binops_62, - add_one_62, - map_add_one_62, - test_func_62, - multiply_62, - num_62, - frac_62, - str_62, - empty_list_62, - mixed_62, - x_63, - y_63, - z_63, - my_str_63, - binops_63, - add_one_63, - map_add_one_63, - test_func_63, - multiply_63, - num_63, - frac_63, - str_63, - empty_list_63, - mixed_63, - x_64, - y_64, - z_64, - my_str_64, - binops_64, - add_one_64, - map_add_one_64, - test_func_64, - multiply_64, - num_64, - frac_64, - str_64, - empty_list_64, - mixed_64, - x_65, - y_65, - z_65, - my_str_65, - binops_65, - add_one_65, - map_add_one_65, - test_func_65, - multiply_65, - num_65, - frac_65, - str_65, - empty_list_65, - mixed_65, - x_66, - y_66, - z_66, - my_str_66, - binops_66, - add_one_66, - map_add_one_66, - test_func_66, - multiply_66, - num_66, - frac_66, - str_66, - empty_list_66, - mixed_66, - x_67, - y_67, - z_67, - my_str_67, - binops_67, - add_one_67, - map_add_one_67, - test_func_67, - multiply_67, - num_67, - frac_67, - str_67, - empty_list_67, - mixed_67, - x_68, - y_68, - z_68, - my_str_68, - binops_68, - add_one_68, - map_add_one_68, - test_func_68, - multiply_68, - num_68, - frac_68, - str_68, - empty_list_68, - mixed_68, - x_69, - y_69, - z_69, - my_str_69, - binops_69, - add_one_69, - map_add_one_69, - test_func_69, - multiply_69, - num_69, - frac_69, - str_69, - empty_list_69, - mixed_69, - x_70, - y_70, - z_70, - my_str_70, - binops_70, - add_one_70, - map_add_one_70, - test_func_70, - multiply_70, - num_70, - frac_70, - str_70, - empty_list_70, - mixed_70, - x_71, - y_71, - z_71, - my_str_71, - binops_71, - add_one_71, - map_add_one_71, - test_func_71, - multiply_71, - num_71, - frac_71, - str_71, - empty_list_71, - mixed_71, - x_72, - y_72, - z_72, - my_str_72, - binops_72, - add_one_72, - map_add_one_72, - test_func_72, - multiply_72, - num_72, - frac_72, - str_72, - empty_list_72, - mixed_72, - x_73, - y_73, - z_73, - my_str_73, - binops_73, - add_one_73, - map_add_one_73, - test_func_73, - multiply_73, - num_73, - frac_73, - str_73, - empty_list_73, - mixed_73, - x_74, - y_74, - z_74, - my_str_74, - binops_74, - add_one_74, - map_add_one_74, - test_func_74, - multiply_74, - num_74, - frac_74, - str_74, - empty_list_74, - mixed_74, - x_75, - y_75, - z_75, - my_str_75, - binops_75, - add_one_75, - map_add_one_75, - test_func_75, - multiply_75, - num_75, - frac_75, - str_75, - empty_list_75, - mixed_75, - x_76, - y_76, - z_76, - my_str_76, - binops_76, - add_one_76, - map_add_one_76, - test_func_76, - multiply_76, - num_76, - frac_76, - str_76, - empty_list_76, - mixed_76, - x_77, - y_77, - z_77, - my_str_77, - binops_77, - add_one_77, - map_add_one_77, - test_func_77, - multiply_77, - num_77, - frac_77, - str_77, - empty_list_77, - mixed_77, - x_78, - y_78, - z_78, - my_str_78, - binops_78, - add_one_78, - map_add_one_78, - test_func_78, - multiply_78, - num_78, - frac_78, - str_78, - empty_list_78, - mixed_78, - x_79, - y_79, - z_79, - my_str_79, - binops_79, - add_one_79, - map_add_one_79, - test_func_79, - multiply_79, - num_79, - frac_79, - str_79, - empty_list_79, - mixed_79, - x_80, - y_80, - z_80, - my_str_80, - binops_80, - add_one_80, - map_add_one_80, - test_func_80, - multiply_80, - num_80, - frac_80, - str_80, - empty_list_80, - mixed_80, - x_81, - y_81, - z_81, - my_str_81, - binops_81, - add_one_81, - map_add_one_81, - test_func_81, - multiply_81, - num_81, - frac_81, - str_81, - empty_list_81, - mixed_81, - x_82, - y_82, - z_82, - my_str_82, - binops_82, - add_one_82, - map_add_one_82, - test_func_82, - multiply_82, - num_82, - frac_82, - str_82, - empty_list_82, - mixed_82, - x_83, - y_83, - z_83, - my_str_83, - binops_83, - add_one_83, - map_add_one_83, - test_func_83, - multiply_83, - num_83, - frac_83, - str_83, - empty_list_83, - mixed_83, - x_84, - y_84, - z_84, - my_str_84, - binops_84, - add_one_84, - map_add_one_84, - test_func_84, - multiply_84, - num_84, - frac_84, - str_84, - empty_list_84, - mixed_84, - x_85, - y_85, - z_85, - my_str_85, - binops_85, - add_one_85, - map_add_one_85, - test_func_85, - multiply_85, - num_85, - frac_85, - str_85, - empty_list_85, - mixed_85, - x_86, - y_86, - z_86, - my_str_86, - binops_86, - add_one_86, - map_add_one_86, - test_func_86, - multiply_86, - num_86, - frac_86, - str_86, - empty_list_86, - mixed_86, - x_87, - y_87, - z_87, - my_str_87, - binops_87, - add_one_87, - map_add_one_87, - test_func_87, - multiply_87, - num_87, - frac_87, - str_87, - empty_list_87, - mixed_87, - x_88, - y_88, - z_88, - my_str_88, - binops_88, - add_one_88, - map_add_one_88, - test_func_88, - multiply_88, - num_88, - frac_88, - str_88, - empty_list_88, - mixed_88, - x_89, - y_89, - z_89, - my_str_89, - binops_89, - add_one_89, - map_add_one_89, - test_func_89, - multiply_89, - num_89, - frac_89, - str_89, - empty_list_89, - mixed_89, - x_90, - y_90, - z_90, - my_str_90, - binops_90, - add_one_90, - map_add_one_90, - test_func_90, - multiply_90, - num_90, - frac_90, - str_90, - empty_list_90, - mixed_90, - x_91, - y_91, - z_91, - my_str_91, - binops_91, - add_one_91, - map_add_one_91, - test_func_91, - multiply_91, - num_91, - frac_91, - str_91, - empty_list_91, - mixed_91, - x_92, - y_92, - z_92, - my_str_92, - binops_92, - add_one_92, - map_add_one_92, - test_func_92, - multiply_92, - num_92, - frac_92, - str_92, - empty_list_92, - mixed_92, - x_93, - y_93, - z_93, - my_str_93, - binops_93, - add_one_93, - map_add_one_93, - test_func_93, - multiply_93, - num_93, - frac_93, - str_93, - empty_list_93, - mixed_93, - x_94, - y_94, - z_94, - my_str_94, - binops_94, - add_one_94, - map_add_one_94, - test_func_94, - multiply_94, - num_94, - frac_94, - str_94, - empty_list_94, - mixed_94, - x_95, - y_95, - z_95, - my_str_95, - binops_95, - add_one_95, - map_add_one_95, - test_func_95, - multiply_95, - num_95, - frac_95, - str_95, - empty_list_95, - mixed_95, - x_96, - y_96, - z_96, - my_str_96, - binops_96, - add_one_96, - map_add_one_96, - test_func_96, - multiply_96, - num_96, - frac_96, - str_96, - empty_list_96, - mixed_96, - x_97, - y_97, - z_97, - my_str_97, - binops_97, - add_one_97, - map_add_one_97, - test_func_97, - multiply_97, - num_97, - frac_97, - str_97, - empty_list_97, - mixed_97, - x_98, - y_98, - z_98, - my_str_98, - binops_98, - add_one_98, - map_add_one_98, - test_func_98, - multiply_98, - num_98, - frac_98, - str_98, - empty_list_98, - mixed_98, - x_99, - y_99, - z_99, - my_str_99, - binops_99, - add_one_99, - map_add_one_99, - test_func_99, - multiply_99, - num_99, - frac_99, - str_99, - empty_list_99, - mixed_99, - x_100, - y_100, - z_100, - my_str_100, - binops_100, - add_one_100, - map_add_one_100, - test_func_100, - multiply_100, - num_100, - frac_100, - str_100, - empty_list_100, - mixed_100, - x_101, - y_101, - z_101, - my_str_101, - binops_101, - add_one_101, - map_add_one_101, - test_func_101, - multiply_101, - num_101, - frac_101, - str_101, - empty_list_101, - mixed_101, - x_102, - y_102, - z_102, - my_str_102, - binops_102, - add_one_102, - map_add_one_102, - test_func_102, - multiply_102, - num_102, - frac_102, - str_102, - empty_list_102, - mixed_102, - x_103, - y_103, - z_103, - my_str_103, - binops_103, - add_one_103, - map_add_one_103, - test_func_103, - multiply_103, - num_103, - frac_103, - str_103, - empty_list_103, - mixed_103, - x_104, - y_104, - z_104, - my_str_104, - binops_104, - add_one_104, - map_add_one_104, - test_func_104, - multiply_104, - num_104, - frac_104, - str_104, - empty_list_104, - mixed_104, - x_105, - y_105, - z_105, - my_str_105, - binops_105, - add_one_105, - map_add_one_105, - test_func_105, - multiply_105, - num_105, - frac_105, - str_105, - empty_list_105, - mixed_105, - x_106, - y_106, - z_106, - my_str_106, - binops_106, - add_one_106, - map_add_one_106, - test_func_106, - multiply_106, - num_106, - frac_106, - str_106, - empty_list_106, - mixed_106, - x_107, - y_107, - z_107, - my_str_107, - binops_107, - add_one_107, - map_add_one_107, - test_func_107, - multiply_107, - num_107, - frac_107, - str_107, - empty_list_107, - mixed_107, - x_108, - y_108, - z_108, - my_str_108, - binops_108, - add_one_108, - map_add_one_108, - test_func_108, - multiply_108, - num_108, - frac_108, - str_108, - empty_list_108, - mixed_108, - x_109, - y_109, - z_109, - my_str_109, - binops_109, - add_one_109, - map_add_one_109, - test_func_109, - multiply_109, - num_109, - frac_109, - str_109, - empty_list_109, - mixed_109, - x_110, - y_110, - z_110, - my_str_110, - binops_110, - add_one_110, - map_add_one_110, - test_func_110, - multiply_110, - num_110, - frac_110, - str_110, - empty_list_110, - mixed_110, - x_111, - y_111, - z_111, - my_str_111, - binops_111, - add_one_111, - map_add_one_111, - test_func_111, - multiply_111, - num_111, - frac_111, - str_111, - empty_list_111, - mixed_111, - x_112, - y_112, - z_112, - my_str_112, - binops_112, - add_one_112, - map_add_one_112, - test_func_112, - multiply_112, - num_112, - frac_112, - str_112, - empty_list_112, - mixed_112, - x_113, - y_113, - z_113, - my_str_113, - binops_113, - add_one_113, - map_add_one_113, - test_func_113, - multiply_113, - num_113, - frac_113, - str_113, - empty_list_113, - mixed_113, - x_114, - y_114, - z_114, - my_str_114, - binops_114, - add_one_114, - map_add_one_114, - test_func_114, - multiply_114, - num_114, - frac_114, - str_114, - empty_list_114, - mixed_114, - x_115, - y_115, - z_115, - my_str_115, - binops_115, - add_one_115, - map_add_one_115, - test_func_115, - multiply_115, - num_115, - frac_115, - str_115, - empty_list_115, - mixed_115, - x_116, - y_116, - z_116, - my_str_116, - binops_116, - add_one_116, - map_add_one_116, - test_func_116, - multiply_116, - num_116, - frac_116, - str_116, - empty_list_116, - mixed_116, - x_117, - y_117, - z_117, - my_str_117, - binops_117, - add_one_117, - map_add_one_117, - test_func_117, - multiply_117, - num_117, - frac_117, - str_117, - empty_list_117, - mixed_117, - x_118, - y_118, - z_118, - my_str_118, - binops_118, - add_one_118, - map_add_one_118, - test_func_118, - multiply_118, - num_118, - frac_118, - str_118, - empty_list_118, - mixed_118, - x_119, - y_119, - z_119, - my_str_119, - binops_119, - add_one_119, - map_add_one_119, - test_func_119, - multiply_119, - num_119, - frac_119, - str_119, - empty_list_119, - mixed_119, - x_120, - y_120, - z_120, - my_str_120, - binops_120, - add_one_120, - map_add_one_120, - test_func_120, - multiply_120, - num_120, - frac_120, - str_120, - empty_list_120, - mixed_120, - x_121, - y_121, - z_121, - my_str_121, - binops_121, - add_one_121, - map_add_one_121, - test_func_121, - multiply_121, - num_121, - frac_121, - str_121, - empty_list_121, - mixed_121, - x_122, - y_122, - z_122, - my_str_122, - binops_122, - add_one_122, - map_add_one_122, - test_func_122, - multiply_122, - num_122, - frac_122, - str_122, - empty_list_122, - mixed_122, - x_123, - y_123, - z_123, - my_str_123, - binops_123, - add_one_123, - map_add_one_123, - test_func_123, - multiply_123, - num_123, - frac_123, - str_123, - empty_list_123, - mixed_123, - x_124, - y_124, - z_124, - my_str_124, - binops_124, - add_one_124, - map_add_one_124, - test_func_124, - multiply_124, - num_124, - frac_124, - str_124, - empty_list_124, - mixed_124, - x_125, - y_125, - z_125, - my_str_125, - binops_125, - add_one_125, - map_add_one_125, - test_func_125, - multiply_125, - num_125, - frac_125, - str_125, - empty_list_125, - mixed_125, - x_126, - y_126, - z_126, - my_str_126, - binops_126, - add_one_126, - map_add_one_126, - test_func_126, - multiply_126, - num_126, - frac_126, - str_126, - empty_list_126, - mixed_126, - x_127, - y_127, - z_127, - my_str_127, - binops_127, - add_one_127, - map_add_one_127, - test_func_127, - multiply_127, - num_127, - frac_127, - str_127, - empty_list_127, - mixed_127, - x_128, - y_128, - z_128, - my_str_128, - binops_128, - add_one_128, - map_add_one_128, - test_func_128, - multiply_128, - num_128, - frac_128, - str_128, - empty_list_128, - mixed_128, - x_129, - y_129, - z_129, - my_str_129, - binops_129, - add_one_129, - map_add_one_129, - test_func_129, - multiply_129, - num_129, - frac_129, - str_129, - empty_list_129, - mixed_129, - x_130, - y_130, - z_130, - my_str_130, - binops_130, - add_one_130, - map_add_one_130, - test_func_130, - multiply_130, - num_130, - frac_130, - str_130, - empty_list_130, - mixed_130, - x_131, - y_131, - z_131, - my_str_131, - binops_131, - add_one_131, - map_add_one_131, - test_func_131, - multiply_131, - num_131, - frac_131, - str_131, - empty_list_131, - mixed_131, - x_132, - y_132, - z_132, - my_str_132, - binops_132, - add_one_132, - map_add_one_132, - test_func_132, - multiply_132, - num_132, - frac_132, - str_132, - empty_list_132, - mixed_132, - x_133, - y_133, - z_133, - my_str_133, - binops_133, - add_one_133, - map_add_one_133, - test_func_133, - multiply_133, - num_133, - frac_133, - str_133, - empty_list_133, - mixed_133, - x_134, - y_134, - z_134, - my_str_134, - binops_134, - add_one_134, - map_add_one_134, - test_func_134, - multiply_134, - num_134, - frac_134, - str_134, - empty_list_134, - mixed_134, - x_135, - y_135, - z_135, - my_str_135, - binops_135, - add_one_135, - map_add_one_135, - test_func_135, - multiply_135, - num_135, - frac_135, - str_135, - empty_list_135, - mixed_135, - x_136, - y_136, - z_136, - my_str_136, - binops_136, - add_one_136, - map_add_one_136, - test_func_136, - multiply_136, - num_136, - frac_136, - str_136, - empty_list_136, - mixed_136, - x_137, - y_137, - z_137, - my_str_137, - binops_137, - add_one_137, - map_add_one_137, - test_func_137, - multiply_137, - num_137, - frac_137, - str_137, - empty_list_137, - mixed_137, - x_138, - y_138, - z_138, - my_str_138, - binops_138, - add_one_138, - map_add_one_138, - test_func_138, - multiply_138, - num_138, - frac_138, - str_138, - empty_list_138, - mixed_138, - x_139, - y_139, - z_139, - my_str_139, - binops_139, - add_one_139, - map_add_one_139, - test_func_139, - multiply_139, - num_139, - frac_139, - str_139, - empty_list_139, - mixed_139, - x_140, - y_140, - z_140, - my_str_140, - binops_140, - add_one_140, - map_add_one_140, - test_func_140, - multiply_140, - num_140, - frac_140, - str_140, - empty_list_140, - mixed_140, - x_141, - y_141, - z_141, - my_str_141, - binops_141, - add_one_141, - map_add_one_141, - test_func_141, - multiply_141, - num_141, - frac_141, - str_141, - empty_list_141, - mixed_141, - x_142, - y_142, - z_142, - my_str_142, - binops_142, - add_one_142, - map_add_one_142, - test_func_142, - multiply_142, - num_142, - frac_142, - str_142, - empty_list_142, - mixed_142, - x_143, - y_143, - z_143, - my_str_143, - binops_143, - add_one_143, - map_add_one_143, - test_func_143, - multiply_143, - num_143, - frac_143, - str_143, - empty_list_143, - mixed_143, - x_144, - y_144, - z_144, - my_str_144, - binops_144, - add_one_144, - map_add_one_144, - test_func_144, - multiply_144, - num_144, - frac_144, - str_144, - empty_list_144, - mixed_144, - x_145, - y_145, - z_145, - my_str_145, - binops_145, - add_one_145, - map_add_one_145, - test_func_145, - multiply_145, - num_145, - frac_145, - str_145, - empty_list_145, - mixed_145, - x_146, - y_146, - z_146, - my_str_146, - binops_146, - add_one_146, - map_add_one_146, - test_func_146, - multiply_146, - num_146, - frac_146, - str_146, - empty_list_146, - mixed_146, - x_147, - y_147, - z_147, - my_str_147, - binops_147, - add_one_147, - map_add_one_147, - test_func_147, - multiply_147, - num_147, - frac_147, - str_147, - empty_list_147, - mixed_147, - x_148, - y_148, - z_148, - my_str_148, - binops_148, - add_one_148, - map_add_one_148, - test_func_148, - multiply_148, - num_148, - frac_148, - str_148, - empty_list_148, - mixed_148, - x_149, - y_149, - z_149, - my_str_149, - binops_149, - add_one_149, - map_add_one_149, - test_func_149, - multiply_149, - num_149, - frac_149, - str_149, - empty_list_149, - mixed_149, - x_150, - y_150, - z_150, - my_str_150, - binops_150, - add_one_150, - map_add_one_150, - test_func_150, - multiply_150, - num_150, - frac_150, - str_150, - empty_list_150, - mixed_150, - x_151, - y_151, - z_151, - my_str_151, - binops_151, - add_one_151, - map_add_one_151, - test_func_151, - multiply_151, - num_151, - frac_151, - str_151, - empty_list_151, - mixed_151, - x_152, - y_152, - z_152, - my_str_152, - binops_152, - add_one_152, - map_add_one_152, - test_func_152, - multiply_152, - num_152, - frac_152, - str_152, - empty_list_152, - mixed_152, - x_153, - y_153, - z_153, - my_str_153, - binops_153, - add_one_153, - map_add_one_153, - test_func_153, - multiply_153, - num_153, - frac_153, - str_153, - empty_list_153, - mixed_153, - x_154, - y_154, - z_154, - my_str_154, - binops_154, - add_one_154, - map_add_one_154, - test_func_154, - multiply_154, - num_154, - frac_154, - str_154, - empty_list_154, - mixed_154, - x_155, - y_155, - z_155, - my_str_155, - binops_155, - add_one_155, - map_add_one_155, - test_func_155, - multiply_155, - num_155, - frac_155, - str_155, - empty_list_155, - mixed_155, - x_156, - y_156, - z_156, - my_str_156, - binops_156, - add_one_156, - map_add_one_156, - test_func_156, - multiply_156, - num_156, - frac_156, - str_156, - empty_list_156, - mixed_156, - x_157, - y_157, - z_157, - my_str_157, - binops_157, - add_one_157, - map_add_one_157, - test_func_157, - multiply_157, - num_157, - frac_157, - str_157, - empty_list_157, - mixed_157, - x_158, - y_158, - z_158, - my_str_158, - binops_158, - add_one_158, - map_add_one_158, - test_func_158, - multiply_158, - num_158, - frac_158, - str_158, - empty_list_158, - mixed_158, - x_159, - y_159, - z_159, - my_str_159, - binops_159, - add_one_159, - map_add_one_159, - test_func_159, - multiply_159, - num_159, - frac_159, - str_159, - empty_list_159, - mixed_159, - x_160, - y_160, - z_160, - my_str_160, - binops_160, - add_one_160, - map_add_one_160, - test_func_160, - multiply_160, - num_160, - frac_160, - str_160, - empty_list_160, - mixed_160, - x_161, - y_161, - z_161, - my_str_161, - binops_161, - add_one_161, - map_add_one_161, - test_func_161, - multiply_161, - num_161, - frac_161, - str_161, - empty_list_161, - mixed_161, - x_162, - y_162, - z_162, - my_str_162, - binops_162, - add_one_162, - map_add_one_162, - test_func_162, - multiply_162, - num_162, - frac_162, - str_162, - empty_list_162, - mixed_162, - x_163, - y_163, - z_163, - my_str_163, - binops_163, - add_one_163, - map_add_one_163, - test_func_163, - multiply_163, - num_163, - frac_163, - str_163, - empty_list_163, - mixed_163, - x_164, - y_164, - z_164, - my_str_164, - binops_164, - add_one_164, - map_add_one_164, - test_func_164, - multiply_164, - num_164, - frac_164, - str_164, - empty_list_164, - mixed_164, - x_165, - y_165, - z_165, - my_str_165, - binops_165, - add_one_165, - map_add_one_165, - test_func_165, - multiply_165, - num_165, - frac_165, - str_165, - empty_list_165, - mixed_165, - x_166, - y_166, - z_166, - my_str_166, - binops_166, - add_one_166, - map_add_one_166, - test_func_166, - multiply_166, - num_166, - frac_166, - str_166, - empty_list_166, - mixed_166, - x_167, - y_167, - z_167, - my_str_167, - binops_167, - add_one_167, - map_add_one_167, - test_func_167, - multiply_167, - num_167, - frac_167, - str_167, - empty_list_167, - mixed_167, - x_168, - y_168, - z_168, - my_str_168, - binops_168, - add_one_168, - map_add_one_168, - test_func_168, - multiply_168, - num_168, - frac_168, - str_168, - empty_list_168, - mixed_168, - x_169, - y_169, - z_169, - my_str_169, - binops_169, - add_one_169, - map_add_one_169, - test_func_169, - multiply_169, - num_169, - frac_169, - str_169, - empty_list_169, - mixed_169, - x_170, - y_170, - z_170, - my_str_170, - binops_170, - add_one_170, - map_add_one_170, - test_func_170, - multiply_170, - num_170, - frac_170, - str_170, - empty_list_170, - mixed_170, - x_171, - y_171, - z_171, - my_str_171, - binops_171, - add_one_171, - map_add_one_171, - test_func_171, - multiply_171, - num_171, - frac_171, - str_171, - empty_list_171, - mixed_171, - x_172, - y_172, - z_172, - my_str_172, - binops_172, - add_one_172, - map_add_one_172, - test_func_172, - multiply_172, - num_172, - frac_172, - str_172, - empty_list_172, - mixed_172, - x_173, - y_173, - z_173, - my_str_173, - binops_173, - add_one_173, - map_add_one_173, - test_func_173, - multiply_173, - num_173, - frac_173, - str_173, - empty_list_173, - mixed_173, - x_174, - y_174, - z_174, - my_str_174, - binops_174, - add_one_174, - map_add_one_174, - test_func_174, - multiply_174, - num_174, - frac_174, - str_174, - empty_list_174, - mixed_174, - x_175, - y_175, - z_175, - my_str_175, - binops_175, - add_one_175, - map_add_one_175, - test_func_175, - multiply_175, - num_175, - frac_175, - str_175, - empty_list_175, - mixed_175, - x_176, - y_176, - z_176, - my_str_176, - binops_176, - add_one_176, - map_add_one_176, - test_func_176, - multiply_176, - num_176, - frac_176, - str_176, - empty_list_176, - mixed_176, - x_177, - y_177, - z_177, - my_str_177, - binops_177, - add_one_177, - map_add_one_177, - test_func_177, - multiply_177, - num_177, - frac_177, - str_177, - empty_list_177, - mixed_177, - x_178, - y_178, - z_178, - my_str_178, - binops_178, - add_one_178, - map_add_one_178, - test_func_178, - multiply_178, - num_178, - frac_178, - str_178, - empty_list_178, - mixed_178, - x_179, - y_179, - z_179, - my_str_179, - binops_179, - add_one_179, - map_add_one_179, - test_func_179, - multiply_179, - num_179, - frac_179, - str_179, - empty_list_179, - mixed_179, - x_180, - y_180, - z_180, - my_str_180, - binops_180, - add_one_180, - map_add_one_180, - test_func_180, - multiply_180, - num_180, - frac_180, - str_180, - empty_list_180, - mixed_180, - x_181, - y_181, - z_181, - my_str_181, - binops_181, - add_one_181, - map_add_one_181, - test_func_181, - multiply_181, - num_181, - frac_181, - str_181, - empty_list_181, - mixed_181, - x_182, - y_182, - z_182, - my_str_182, - binops_182, - add_one_182, - map_add_one_182, - test_func_182, - multiply_182, - num_182, - frac_182, - str_182, - empty_list_182, - mixed_182, - x_183, - y_183, - z_183, - my_str_183, - binops_183, - add_one_183, - map_add_one_183, - test_func_183, - multiply_183, - num_183, - frac_183, - str_183, - empty_list_183, - mixed_183, - x_184, - y_184, - z_184, - my_str_184, - binops_184, - add_one_184, - map_add_one_184, - test_func_184, - multiply_184, - num_184, - frac_184, - str_184, - empty_list_184, - mixed_184, - x_185, - y_185, - z_185, - my_str_185, - binops_185, - add_one_185, - map_add_one_185, - test_func_185, - multiply_185, - num_185, - frac_185, - str_185, - empty_list_185, - mixed_185, - x_186, - y_186, - z_186, - my_str_186, - binops_186, - add_one_186, - map_add_one_186, - test_func_186, - multiply_186, - num_186, - frac_186, - str_186, - empty_list_186, - mixed_186, - x_187, - y_187, - z_187, - my_str_187, - binops_187, - add_one_187, - map_add_one_187, - test_func_187, - multiply_187, - num_187, - frac_187, - str_187, - empty_list_187, - mixed_187, - x_188, - y_188, - z_188, - my_str_188, - binops_188, - add_one_188, - map_add_one_188, - test_func_188, - multiply_188, - num_188, - frac_188, - str_188, - empty_list_188, - mixed_188, - x_189, - y_189, - z_189, - my_str_189, - binops_189, - add_one_189, - map_add_one_189, - test_func_189, - multiply_189, - num_189, - frac_189, - str_189, - empty_list_189, - mixed_189, - x_190, - y_190, - z_190, - my_str_190, - binops_190, - add_one_190, - map_add_one_190, - test_func_190, - multiply_190, - num_190, - frac_190, - str_190, - empty_list_190, - mixed_190, - x_191, - y_191, - z_191, - my_str_191, - binops_191, - add_one_191, - map_add_one_191, - test_func_191, - multiply_191, - num_191, - frac_191, - str_191, - empty_list_191, - mixed_191, - x_192, - y_192, - z_192, - my_str_192, - binops_192, - add_one_192, - map_add_one_192, - test_func_192, - multiply_192, - num_192, - frac_192, - str_192, - empty_list_192, - mixed_192, - x_193, - y_193, - z_193, - my_str_193, - binops_193, - add_one_193, - map_add_one_193, - test_func_193, - multiply_193, - num_193, - frac_193, - str_193, - empty_list_193, - mixed_193, - x_194, - y_194, - z_194, - my_str_194, - binops_194, - add_one_194, - map_add_one_194, - test_func_194, - multiply_194, - num_194, - frac_194, - str_194, - empty_list_194, - mixed_194, - x_195, - y_195, - z_195, - my_str_195, - binops_195, - add_one_195, - map_add_one_195, - test_func_195, - multiply_195, - num_195, - frac_195, - str_195, - empty_list_195, - mixed_195, - x_196, - y_196, - z_196, - my_str_196, - binops_196, - add_one_196, - map_add_one_196, - test_func_196, - multiply_196, - num_196, - frac_196, - str_196, - empty_list_196, - mixed_196, - x_197, - y_197, - z_197, - my_str_197, - binops_197, - add_one_197, - map_add_one_197, - test_func_197, - multiply_197, - num_197, - frac_197, - str_197, - empty_list_197, - mixed_197, - x_198, - y_198, - z_198, - my_str_198, - binops_198, - add_one_198, - map_add_one_198, - test_func_198, - multiply_198, - num_198, - frac_198, - str_198, - empty_list_198, - mixed_198, - x_199, - y_199, - z_199, - my_str_199, - binops_199, - add_one_199, - map_add_one_199, - test_func_199, - multiply_199, - num_199, - frac_199, - str_199, - empty_list_199, - mixed_199, - x_200, - y_200, - z_200, - my_str_200, - binops_200, - add_one_200, - map_add_one_200, - test_func_200, - multiply_200, - num_200, - frac_200, - str_200, - empty_list_200, - mixed_200, - x_201, - y_201, - z_201, - my_str_201, - binops_201, - add_one_201, - map_add_one_201, - test_func_201, - multiply_201, - num_201, - frac_201, - str_201, - empty_list_201, - mixed_201, - x_202, - y_202, - z_202, - my_str_202, - binops_202, - add_one_202, - map_add_one_202, - test_func_202, - multiply_202, - num_202, - frac_202, - str_202, - empty_list_202, - mixed_202, - x_203, - y_203, - z_203, - my_str_203, - binops_203, - add_one_203, - map_add_one_203, - test_func_203, - multiply_203, - num_203, - frac_203, - str_203, - empty_list_203, - mixed_203, - x_204, - y_204, - z_204, - my_str_204, - binops_204, - add_one_204, - map_add_one_204, - test_func_204, - multiply_204, - num_204, - frac_204, - str_204, - empty_list_204, - mixed_204, - x_205, - y_205, - z_205, - my_str_205, - binops_205, - add_one_205, - map_add_one_205, - test_func_205, - multiply_205, - num_205, - frac_205, - str_205, - empty_list_205, - mixed_205, - x_206, - y_206, - z_206, - my_str_206, - binops_206, - add_one_206, - map_add_one_206, - test_func_206, - multiply_206, - num_206, - frac_206, - str_206, - empty_list_206, - mixed_206, - x_207, - y_207, - z_207, - my_str_207, - binops_207, - add_one_207, - map_add_one_207, - test_func_207, - multiply_207, - num_207, - frac_207, - str_207, - empty_list_207, - mixed_207, - x_208, - y_208, - z_208, - my_str_208, - binops_208, - add_one_208, - map_add_one_208, - test_func_208, - multiply_208, - num_208, - frac_208, - str_208, - empty_list_208, - mixed_208, - x_209, - y_209, - z_209, - my_str_209, - binops_209, - add_one_209, - map_add_one_209, - test_func_209, - multiply_209, - num_209, - frac_209, - str_209, - empty_list_209, - mixed_209, - x_210, - y_210, - z_210, - my_str_210, - binops_210, - add_one_210, - map_add_one_210, - test_func_210, - multiply_210, - num_210, - frac_210, - str_210, - empty_list_210, - mixed_210, - x_211, - y_211, - z_211, - my_str_211, - binops_211, - add_one_211, - map_add_one_211, - test_func_211, - multiply_211, - num_211, - frac_211, - str_211, - empty_list_211, - mixed_211, - x_212, - y_212, - z_212, - my_str_212, - binops_212, - add_one_212, - map_add_one_212, - test_func_212, - multiply_212, - num_212, - frac_212, - str_212, - empty_list_212, - mixed_212, - x_213, - y_213, - z_213, - my_str_213, - binops_213, - add_one_213, - map_add_one_213, - test_func_213, - multiply_213, - num_213, - frac_213, - str_213, - empty_list_213, - mixed_213, - x_214, - y_214, - z_214, - my_str_214, - binops_214, - add_one_214, - map_add_one_214, - test_func_214, - multiply_214, - num_214, - frac_214, - str_214, - empty_list_214, - mixed_214, - x_215, - y_215, - z_215, - my_str_215, - binops_215, - add_one_215, - map_add_one_215, - test_func_215, - multiply_215, - num_215, - frac_215, - str_215, - empty_list_215, - mixed_215, - x_216, - y_216, - z_216, - my_str_216, - binops_216, - add_one_216, - map_add_one_216, - test_func_216, - multiply_216, - num_216, - frac_216, - str_216, - empty_list_216, - mixed_216, - x_217, - y_217, - z_217, - my_str_217, - binops_217, - add_one_217, - map_add_one_217, - test_func_217, - multiply_217, - num_217, - frac_217, - str_217, - empty_list_217, - mixed_217, - x_218, - y_218, - z_218, - my_str_218, - binops_218, - add_one_218, - map_add_one_218, - test_func_218, - multiply_218, - num_218, - frac_218, - str_218, - empty_list_218, - mixed_218, - x_219, - y_219, - z_219, - my_str_219, - binops_219, - add_one_219, - map_add_one_219, - test_func_219, - multiply_219, - num_219, - frac_219, - str_219, - empty_list_219, - mixed_219, - x_220, - y_220, - z_220, - my_str_220, - binops_220, - add_one_220, - map_add_one_220, - test_func_220, - multiply_220, - num_220, - frac_220, - str_220, - empty_list_220, - mixed_220, - x_221, - y_221, - z_221, - my_str_221, - binops_221, - add_one_221, - map_add_one_221, - test_func_221, - multiply_221, - num_221, - frac_221, - str_221, - empty_list_221, - mixed_221, - x_222, - y_222, - z_222, - my_str_222, - binops_222, - add_one_222, - map_add_one_222, - test_func_222, - multiply_222, - num_222, - frac_222, - str_222, - empty_list_222, - mixed_222, - x_223, - y_223, - z_223, - my_str_223, - binops_223, - add_one_223, - map_add_one_223, - test_func_223, - multiply_223, - num_223, - frac_223, - str_223, - empty_list_223, - mixed_223, - x_224, - y_224, - z_224, - my_str_224, - binops_224, - add_one_224, - map_add_one_224, - test_func_224, - multiply_224, - num_224, - frac_224, - str_224, - empty_list_224, - mixed_224, - x_225, - y_225, - z_225, - my_str_225, - binops_225, - add_one_225, - map_add_one_225, - test_func_225, - multiply_225, - num_225, - frac_225, - str_225, - empty_list_225, - mixed_225, - x_226, - y_226, - z_226, - my_str_226, - binops_226, - add_one_226, - map_add_one_226, - test_func_226, - multiply_226, - num_226, - frac_226, - str_226, - empty_list_226, - mixed_226, - x_227, - y_227, - z_227, - my_str_227, - binops_227, - add_one_227, - map_add_one_227, - test_func_227, - multiply_227, - num_227, - frac_227, - str_227, - empty_list_227, - mixed_227, - x_228, - y_228, - z_228, - my_str_228, - binops_228, - add_one_228, - map_add_one_228, - test_func_228, - multiply_228, - num_228, - frac_228, - str_228, - empty_list_228, - mixed_228, - x_229, - y_229, - z_229, - my_str_229, - binops_229, - add_one_229, - map_add_one_229, - test_func_229, - multiply_229, - num_229, - frac_229, - str_229, - empty_list_229, - mixed_229, - x_230, - y_230, - z_230, - my_str_230, - binops_230, - add_one_230, - map_add_one_230, - test_func_230, - multiply_230, - num_230, - frac_230, - str_230, - empty_list_230, - mixed_230, - x_231, - y_231, - z_231, - my_str_231, - binops_231, - add_one_231, - map_add_one_231, - test_func_231, - multiply_231, - num_231, - frac_231, - str_231, - empty_list_231, - mixed_231, - x_232, - y_232, - z_232, - my_str_232, - binops_232, - add_one_232, - map_add_one_232, - test_func_232, - multiply_232, - num_232, - frac_232, - str_232, - empty_list_232, - mixed_232, - x_233, - y_233, - z_233, - my_str_233, - binops_233, - add_one_233, - map_add_one_233, - test_func_233, - multiply_233, - num_233, - frac_233, - str_233, - empty_list_233, - mixed_233, - x_234, - y_234, - z_234, - my_str_234, - binops_234, - add_one_234, - map_add_one_234, - test_func_234, - multiply_234, - num_234, - frac_234, - str_234, - empty_list_234, - mixed_234, - x_235, - y_235, - z_235, - my_str_235, - binops_235, - add_one_235, - map_add_one_235, - test_func_235, - multiply_235, - num_235, - frac_235, - str_235, - empty_list_235, - mixed_235, - x_236, - y_236, - z_236, - my_str_236, - binops_236, - add_one_236, - map_add_one_236, - test_func_236, - multiply_236, - num_236, - frac_236, - str_236, - empty_list_236, - mixed_236, - x_237, - y_237, - z_237, - my_str_237, - binops_237, - add_one_237, - map_add_one_237, - test_func_237, - multiply_237, - num_237, - frac_237, - str_237, - empty_list_237, - mixed_237, - x_238, - y_238, - z_238, - my_str_238, - binops_238, - add_one_238, - map_add_one_238, - test_func_238, - multiply_238, - num_238, - frac_238, - str_238, - empty_list_238, - mixed_238, - x_239, - y_239, - z_239, - my_str_239, - binops_239, - add_one_239, - map_add_one_239, - test_func_239, - multiply_239, - num_239, - frac_239, - str_239, - empty_list_239, - mixed_239, - x_240, - y_240, - z_240, - my_str_240, - binops_240, - add_one_240, - map_add_one_240, - test_func_240, - multiply_240, - num_240, - frac_240, - str_240, - empty_list_240, - mixed_240, - x_241, - y_241, - z_241, - my_str_241, - binops_241, - add_one_241, - map_add_one_241, - test_func_241, - multiply_241, - num_241, - frac_241, - str_241, - empty_list_241, - mixed_241, - x_242, - y_242, - z_242, - my_str_242, - binops_242, - add_one_242, - map_add_one_242, - test_func_242, - multiply_242, - num_242, - frac_242, - str_242, - empty_list_242, - mixed_242, - x_243, - y_243, - z_243, - my_str_243, - binops_243, - add_one_243, - map_add_one_243, - test_func_243, - multiply_243, - num_243, - frac_243, - str_243, - empty_list_243, - mixed_243, - x_244, - y_244, - z_244, - my_str_244, - binops_244, - add_one_244, - map_add_one_244, - test_func_244, - multiply_244, - num_244, - frac_244, - str_244, - empty_list_244, - mixed_244, - x_245, - y_245, - z_245, - my_str_245, - binops_245, - add_one_245, - map_add_one_245, - test_func_245, - multiply_245, - num_245, - frac_245, - str_245, - empty_list_245, - mixed_245, - x_246, - y_246, - z_246, - my_str_246, - binops_246, - add_one_246, - map_add_one_246, - test_func_246, - multiply_246, - num_246, - frac_246, - str_246, - empty_list_246, - mixed_246, - x_247, - y_247, - z_247, - my_str_247, - binops_247, - add_one_247, - map_add_one_247, - test_func_247, - multiply_247, - num_247, - frac_247, - str_247, - empty_list_247, - mixed_247, - x_248, - y_248, - z_248, - my_str_248, - binops_248, - add_one_248, - map_add_one_248, - test_func_248, - multiply_248, - num_248, - frac_248, - str_248, - empty_list_248, - mixed_248, - x_249, - y_249, - z_249, - my_str_249, - binops_249, - add_one_249, - map_add_one_249, - test_func_249, - multiply_249, - num_249, - frac_249, - str_249, - empty_list_249, - mixed_249, - x_250, - y_250, - z_250, - my_str_250, - binops_250, - add_one_250, - map_add_one_250, - test_func_250, - multiply_250, - num_250, - frac_250, - str_250, - empty_list_250, - mixed_250, - x_251, - y_251, - z_251, - my_str_251, - binops_251, - add_one_251, - map_add_one_251, - test_func_251, - multiply_251, - num_251, - frac_251, - str_251, - empty_list_251, - mixed_251, - x_252, - y_252, - z_252, - my_str_252, - binops_252, - add_one_252, - map_add_one_252, - test_func_252, - multiply_252, - num_252, - frac_252, - str_252, - empty_list_252, - mixed_252, - x_253, - y_253, - z_253, - my_str_253, - binops_253, - add_one_253, - map_add_one_253, - test_func_253, - multiply_253, - num_253, - frac_253, - str_253, - empty_list_253, - mixed_253, - x_254, - y_254, - z_254, - my_str_254, - binops_254, - add_one_254, - map_add_one_254, - test_func_254, - multiply_254, - num_254, - frac_254, - str_254, - empty_list_254, - mixed_254, - x_255, - y_255, - z_255, - my_str_255, - binops_255, - add_one_255, - map_add_one_255, - test_func_255, - multiply_255, - num_255, - frac_255, - str_255, - empty_list_255, - mixed_255, - x_256, - y_256, - z_256, - my_str_256, - binops_256, - add_one_256, - map_add_one_256, - test_func_256, - multiply_256, - num_256, - frac_256, - str_256, - empty_list_256, - mixed_256, - x_257, - y_257, - z_257, - my_str_257, - binops_257, - add_one_257, - map_add_one_257, - test_func_257, - multiply_257, - num_257, - frac_257, - str_257, - empty_list_257, - mixed_257, - x_258, - y_258, - z_258, - my_str_258, - binops_258, - add_one_258, - map_add_one_258, - test_func_258, - multiply_258, - num_258, - frac_258, - str_258, - empty_list_258, - mixed_258, - x_259, - y_259, - z_259, - my_str_259, - binops_259, - add_one_259, - map_add_one_259, - test_func_259, - multiply_259, - num_259, - frac_259, - str_259, - empty_list_259, - mixed_259, - x_260, - y_260, - z_260, - my_str_260, - binops_260, - add_one_260, - map_add_one_260, - test_func_260, - multiply_260, - num_260, - frac_260, - str_260, - empty_list_260, - mixed_260, - x_261, - y_261, - z_261, - my_str_261, - binops_261, - add_one_261, - map_add_one_261, - test_func_261, - multiply_261, - num_261, - frac_261, - str_261, - empty_list_261, - mixed_261, - x_262, - y_262, - z_262, - my_str_262, - binops_262, - add_one_262, - map_add_one_262, - test_func_262, - multiply_262, - num_262, - frac_262, - str_262, - empty_list_262, - mixed_262, - x_263, - y_263, - z_263, - my_str_263, - binops_263, - add_one_263, - map_add_one_263, - test_func_263, - multiply_263, - num_263, - frac_263, - str_263, - empty_list_263, - mixed_263, - x_264, - y_264, - z_264, - my_str_264, - binops_264, - add_one_264, - map_add_one_264, - test_func_264, - multiply_264, - num_264, - frac_264, - str_264, - empty_list_264, - mixed_264, - x_265, - y_265, - z_265, - my_str_265, - binops_265, - add_one_265, - map_add_one_265, - test_func_265, - multiply_265, - num_265, - frac_265, - str_265, - empty_list_265, - mixed_265, - x_266, - y_266, - z_266, - my_str_266, - binops_266, - add_one_266, - map_add_one_266, - test_func_266, - multiply_266, - num_266, - frac_266, - str_266, - empty_list_266, - mixed_266, - x_267, - y_267, - z_267, - my_str_267, - binops_267, - add_one_267, - map_add_one_267, - test_func_267, - multiply_267, - num_267, - frac_267, - str_267, - empty_list_267, - mixed_267, - x_268, - y_268, - z_268, - my_str_268, - binops_268, - add_one_268, - map_add_one_268, - test_func_268, - multiply_268, - num_268, - frac_268, - str_268, - empty_list_268, - mixed_268, - x_269, - y_269, - z_269, - my_str_269, - binops_269, - add_one_269, - map_add_one_269, - test_func_269, - multiply_269, - num_269, - frac_269, - str_269, - empty_list_269, - mixed_269, - x_270, - y_270, - z_270, - my_str_270, - binops_270, - add_one_270, - map_add_one_270, - test_func_270, - multiply_270, - num_270, - frac_270, - str_270, - empty_list_270, - mixed_270, - x_271, - y_271, - z_271, - my_str_271, - binops_271, - add_one_271, - map_add_one_271, - test_func_271, - multiply_271, - num_271, - frac_271, - str_271, - empty_list_271, - mixed_271, - x_272, - y_272, - z_272, - my_str_272, - binops_272, - add_one_272, - map_add_one_272, - test_func_272, - multiply_272, - num_272, - frac_272, - str_272, - empty_list_272, - mixed_272, - x_273, - y_273, - z_273, - my_str_273, - binops_273, - add_one_273, - map_add_one_273, - test_func_273, - multiply_273, - num_273, - frac_273, - str_273, - empty_list_273, - mixed_273, - x_274, - y_274, - z_274, - my_str_274, - binops_274, - add_one_274, - map_add_one_274, - test_func_274, - multiply_274, - num_274, - frac_274, - str_274, - empty_list_274, - mixed_274, - x_275, - y_275, - z_275, - my_str_275, - binops_275, - add_one_275, - map_add_one_275, - test_func_275, - multiply_275, - num_275, - frac_275, - str_275, - empty_list_275, - mixed_275, - x_276, - y_276, - z_276, - my_str_276, - binops_276, - add_one_276, - map_add_one_276, - test_func_276, - multiply_276, - num_276, - frac_276, - str_276, - empty_list_276, - mixed_276, - x_277, - y_277, - z_277, - my_str_277, - binops_277, - add_one_277, - map_add_one_277, - test_func_277, - multiply_277, - num_277, - frac_277, - str_277, - empty_list_277, - mixed_277, - x_278, - y_278, - z_278, - my_str_278, - binops_278, - add_one_278, - map_add_one_278, - test_func_278, - multiply_278, - num_278, - frac_278, - str_278, - empty_list_278, - mixed_278, - x_279, - y_279, - z_279, - my_str_279, - binops_279, - add_one_279, - map_add_one_279, - test_func_279, - multiply_279, - num_279, - frac_279, - str_279, - empty_list_279, - mixed_279, - x_280, - y_280, - z_280, - my_str_280, - binops_280, - add_one_280, - map_add_one_280, - test_func_280, - multiply_280, - num_280, - frac_280, - str_280, - empty_list_280, - mixed_280, - x_281, - y_281, - z_281, - my_str_281, - binops_281, - add_one_281, - map_add_one_281, - test_func_281, - multiply_281, - num_281, - frac_281, - str_281, - empty_list_281, - mixed_281, - x_282, - y_282, - z_282, - my_str_282, - binops_282, - add_one_282, - map_add_one_282, - test_func_282, - multiply_282, - num_282, - frac_282, - str_282, - empty_list_282, - mixed_282, - x_283, - y_283, - z_283, - my_str_283, - binops_283, - add_one_283, - map_add_one_283, - test_func_283, - multiply_283, - num_283, - frac_283, - str_283, - empty_list_283, - mixed_283, - x_284, - y_284, - z_284, - my_str_284, - binops_284, - add_one_284, - map_add_one_284, - test_func_284, - multiply_284, - num_284, - frac_284, - str_284, - empty_list_284, - mixed_284, - x_285, - y_285, - z_285, - my_str_285, - binops_285, - add_one_285, - map_add_one_285, - test_func_285, - multiply_285, - num_285, - frac_285, - str_285, - empty_list_285, - mixed_285, - x_286, - y_286, - z_286, - my_str_286, - binops_286, - add_one_286, - map_add_one_286, - test_func_286, - multiply_286, - num_286, - frac_286, - str_286, - empty_list_286, - mixed_286, - x_287, - y_287, - z_287, - my_str_287, - binops_287, - add_one_287, - map_add_one_287, - test_func_287, - multiply_287, - num_287, - frac_287, - str_287, - empty_list_287, - mixed_287, - x_288, - y_288, - z_288, - my_str_288, - binops_288, - add_one_288, - map_add_one_288, - test_func_288, - multiply_288, - num_288, - frac_288, - str_288, - empty_list_288, - mixed_288, - x_289, - y_289, - z_289, - my_str_289, - binops_289, - add_one_289, - map_add_one_289, - test_func_289, - multiply_289, - num_289, - frac_289, - str_289, - empty_list_289, - mixed_289, - x_290, - y_290, - z_290, - my_str_290, - binops_290, - add_one_290, - map_add_one_290, - test_func_290, - multiply_290, - num_290, - frac_290, - str_290, - empty_list_290, - mixed_290, - x_291, - y_291, - z_291, - my_str_291, - binops_291, - add_one_291, - map_add_one_291, - test_func_291, - multiply_291, - num_291, - frac_291, - str_291, - empty_list_291, - mixed_291, - x_292, - y_292, - z_292, - my_str_292, - binops_292, - add_one_292, - map_add_one_292, - test_func_292, - multiply_292, - num_292, - frac_292, - str_292, - empty_list_292, - mixed_292, - x_293, - y_293, - z_293, - my_str_293, - binops_293, - add_one_293, - map_add_one_293, - test_func_293, - multiply_293, - num_293, - frac_293, - str_293, - empty_list_293, - mixed_293, - x_294, - y_294, - z_294, - my_str_294, - binops_294, - add_one_294, - map_add_one_294, - test_func_294, - multiply_294, - num_294, - frac_294, - str_294, - empty_list_294, - mixed_294, - x_295, - y_295, - z_295, - my_str_295, - binops_295, - add_one_295, - map_add_one_295, - test_func_295, - multiply_295, - num_295, - frac_295, - str_295, - empty_list_295, - mixed_295, - x_296, - y_296, - z_296, - my_str_296, - binops_296, - add_one_296, - map_add_one_296, - test_func_296, - multiply_296, - num_296, - frac_296, - str_296, - empty_list_296, - mixed_296, - x_297, - y_297, - z_297, - my_str_297, - binops_297, - add_one_297, - map_add_one_297, - test_func_297, - multiply_297, - num_297, - frac_297, - str_297, - empty_list_297, - mixed_297, - x_298, - y_298, - z_298, - my_str_298, - binops_298, - add_one_298, - map_add_one_298, - test_func_298, - multiply_298, - num_298, - frac_298, - str_298, - empty_list_298, - mixed_298, - x_299, - y_299, - z_299, - my_str_299, - binops_299, - add_one_299, - map_add_one_299, - test_func_299, - multiply_299, - num_299, - frac_299, - str_299, - empty_list_299, - mixed_299, - x_300, - y_300, - z_300, - my_str_300, - binops_300, - add_one_300, - map_add_one_300, - test_func_300, - multiply_300, - num_300, - frac_300, - str_300, - empty_list_300, - mixed_300, - x_301, - y_301, - z_301, - my_str_301, - binops_301, - add_one_301, - map_add_one_301, - test_func_301, - multiply_301, - num_301, - frac_301, - str_301, - empty_list_301, - mixed_301, -] - -x = 3.14 -y = 1.23e45 -z = 0.5 - -my_str : Str -my_str = "one" - -binops = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one : U64 -> U64 -add_one = |n| n + 1 - -map_add_one = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply = |arg_one, arg_two| arg_one * arg_two - -num = 42 -frac = 4.2 -str = "hello" - -# Polymorphic empty collections -empty_list = [] - -# Mixed polymorphic structures -mixed = { - numbers: { value: num, list: [num, num], float: frac }, - strings: { value: str, list: [str, str] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list }, - }, - computations: { - from_num: num * 100, - from_frac: frac * 10.0, - list_from_num: [num, num, num], - }, -} - -x_2 = 3.14 -y_2 = 1.23e45 -z_2 = 0.5 - -my_str_2 : Str -my_str_2 = "one" - -binops_2 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_2 : U64 -> U64 -add_one_2 = |n| n + 1 - -map_add_one_2 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_2 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_2 = |arg_one, arg_two| arg_one * arg_two - -num_2 = 42 -frac_2 = 4.2 -str_2 = "hello" - -# Polymorphic empty collections -empty_list_2 = [] - -# Mixed polymorphic structures -mixed_2 = { - numbers: { value: num_2, list: [num_2, num_2], float: frac }, - strings: { value: str_2, list: [str_2, str_2] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_2 }, - }, - computations: { - from_num: num_2 * 100, - from_frac: frac_2 * 10.0, - list_from_num: [num_2, num_2, num_2], - }, -} - -x_3 = 3.14 -y_3 = 1.23e45 -z_3 = 0.5 - -my_str_3 : Str -my_str_3 = "one" - -binops_3 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_3 : U64 -> U64 -add_one_3 = |n| n + 1 - -map_add_one_3 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_3 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_3 = |arg_one, arg_two| arg_one * arg_two - -num_3 = 42 -frac_3 = 4.2 -str_3 = "hello" - -# Polymorphic empty collections -empty_list_3 = [] - -# Mixed polymorphic structures -mixed_3 = { - numbers: { value: num_3, list: [num_3, num_3], float: frac }, - strings: { value: str_3, list: [str_3, str_3] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_3 }, - }, - computations: { - from_num: num_3 * 100, - from_frac: frac_3 * 10.0, - list_from_num: [num_3, num_3, num_3], - }, -} - -x_4 = 3.14 -y_4 = 1.23e45 -z_4 = 0.5 - -my_str_4 : Str -my_str_4 = "one" - -binops_4 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_4 : U64 -> U64 -add_one_4 = |n| n + 1 - -map_add_one_4 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_4 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_4 = |arg_one, arg_two| arg_one * arg_two - -num_4 = 42 -frac_4 = 4.2 -str_4 = "hello" - -# Polymorphic empty collections -empty_list_4 = [] - -# Mixed polymorphic structures -mixed_4 = { - numbers: { value: num_4, list: [num_4, num_4], float: frac }, - strings: { value: str_4, list: [str_4, str_4] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_4 }, - }, - computations: { - from_num: num_4 * 100, - from_frac: frac_4 * 10.0, - list_from_num: [num_4, num_4, num_4], - }, -} - -x_5 = 3.14 -y_5 = 1.23e45 -z_5 = 0.5 - -my_str_5 : Str -my_str_5 = "one" - -binops_5 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_5 : U64 -> U64 -add_one_5 = |n| n + 1 - -map_add_one_5 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_5 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_5 = |arg_one, arg_two| arg_one * arg_two - -num_5 = 42 -frac_5 = 4.2 -str_5 = "hello" - -# Polymorphic empty collections -empty_list_5 = [] - -# Mixed polymorphic structures -mixed_5 = { - numbers: { value: num_5, list: [num_5, num_5], float: frac }, - strings: { value: str_5, list: [str_5, str_5] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_5 }, - }, - computations: { - from_num: num_5 * 100, - from_frac: frac_5 * 10.0, - list_from_num: [num_5, num_5, num_5], - }, -} - -x_6 = 3.14 -y_6 = 1.23e45 -z_6 = 0.5 - -my_str_6 : Str -my_str_6 = "one" - -binops_6 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_6 : U64 -> U64 -add_one_6 = |n| n + 1 - -map_add_one_6 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_6 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_6 = |arg_one, arg_two| arg_one * arg_two - -num_6 = 42 -frac_6 = 4.2 -str_6 = "hello" - -# Polymorphic empty collections -empty_list_6 = [] - -# Mixed polymorphic structures -mixed_6 = { - numbers: { value: num_6, list: [num_6, num_6], float: frac }, - strings: { value: str_6, list: [str_6, str_6] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_6 }, - }, - computations: { - from_num: num_6 * 100, - from_frac: frac_6 * 10.0, - list_from_num: [num_6, num_6, num_6], - }, -} - -x_7 = 3.14 -y_7 = 1.23e45 -z_7 = 0.5 - -my_str_7 : Str -my_str_7 = "one" - -binops_7 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_7 : U64 -> U64 -add_one_7 = |n| n + 1 - -map_add_one_7 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_7 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_7 = |arg_one, arg_two| arg_one * arg_two - -num_7 = 42 -frac_7 = 4.2 -str_7 = "hello" - -# Polymorphic empty collections -empty_list_7 = [] - -# Mixed polymorphic structures -mixed_7 = { - numbers: { value: num_7, list: [num_7, num_7], float: frac }, - strings: { value: str_7, list: [str_7, str_7] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_7 }, - }, - computations: { - from_num: num_7 * 100, - from_frac: frac_7 * 10.0, - list_from_num: [num_7, num_7, num_7], - }, -} - -x_8 = 3.14 -y_8 = 1.23e45 -z_8 = 0.5 - -my_str_8 : Str -my_str_8 = "one" - -binops_8 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_8 : U64 -> U64 -add_one_8 = |n| n + 1 - -map_add_one_8 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_8 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_8 = |arg_one, arg_two| arg_one * arg_two - -num_8 = 42 -frac_8 = 4.2 -str_8 = "hello" - -# Polymorphic empty collections -empty_list_8 = [] - -# Mixed polymorphic structures -mixed_8 = { - numbers: { value: num_8, list: [num_8, num_8], float: frac }, - strings: { value: str_8, list: [str_8, str_8] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_8 }, - }, - computations: { - from_num: num_8 * 100, - from_frac: frac_8 * 10.0, - list_from_num: [num_8, num_8, num_8], - }, -} - -x_9 = 3.14 -y_9 = 1.23e45 -z_9 = 0.5 - -my_str_9 : Str -my_str_9 = "one" - -binops_9 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_9 : U64 -> U64 -add_one_9 = |n| n + 1 - -map_add_one_9 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_9 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_9 = |arg_one, arg_two| arg_one * arg_two - -num_9 = 42 -frac_9 = 4.2 -str_9 = "hello" - -# Polymorphic empty collections -empty_list_9 = [] - -# Mixed polymorphic structures -mixed_9 = { - numbers: { value: num_9, list: [num_9, num_9], float: frac }, - strings: { value: str_9, list: [str_9, str_9] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_9 }, - }, - computations: { - from_num: num_9 * 100, - from_frac: frac_9 * 10.0, - list_from_num: [num_9, num_9, num_9], - }, -} - -x_10 = 3.14 -y_10 = 1.23e45 -z_10 = 0.5 - -my_str_10 : Str -my_str_10 = "one" - -binops_10 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_10 : U64 -> U64 -add_one_10 = |n| n + 1 - -map_add_one_10 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_10 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_10 = |arg_one, arg_two| arg_one * arg_two - -num_10 = 42 -frac_10 = 4.2 -str_10 = "hello" - -# Polymorphic empty collections -empty_list_10 = [] - -# Mixed polymorphic structures -mixed_10 = { - numbers: { value: num_10, list: [num_10, num_10], float: frac }, - strings: { value: str_10, list: [str_10, str_10] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_10 }, - }, - computations: { - from_num: num_10 * 100, - from_frac: frac_10 * 10.0, - list_from_num: [num_10, num_10, num_10], - }, -} - -x_11 = 3.14 -y_11 = 1.23e45 -z_11 = 0.5 - -my_str_11 : Str -my_str_11 = "one" - -binops_11 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_11 : U64 -> U64 -add_one_11 = |n| n + 1 - -map_add_one_11 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_11 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_11 = |arg_one, arg_two| arg_one * arg_two - -num_11 = 42 -frac_11 = 4.2 -str_11 = "hello" - -# Polymorphic empty collections -empty_list_11 = [] - -# Mixed polymorphic structures -mixed_11 = { - numbers: { value: num_11, list: [num_11, num_11], float: frac }, - strings: { value: str_11, list: [str_11, str_11] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_11 }, - }, - computations: { - from_num: num_11 * 100, - from_frac: frac_11 * 10.0, - list_from_num: [num_11, num_11, num_11], - }, -} - -x_12 = 3.14 -y_12 = 1.23e45 -z_12 = 0.5 - -my_str_12 : Str -my_str_12 = "one" - -binops_12 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_12 : U64 -> U64 -add_one_12 = |n| n + 1 - -map_add_one_12 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_12 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_12 = |arg_one, arg_two| arg_one * arg_two - -num_12 = 42 -frac_12 = 4.2 -str_12 = "hello" - -# Polymorphic empty collections -empty_list_12 = [] - -# Mixed polymorphic structures -mixed_12 = { - numbers: { value: num_12, list: [num_12, num_12], float: frac }, - strings: { value: str_12, list: [str_12, str_12] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_12 }, - }, - computations: { - from_num: num_12 * 100, - from_frac: frac_12 * 10.0, - list_from_num: [num_12, num_12, num_12], - }, -} - -x_13 = 3.14 -y_13 = 1.23e45 -z_13 = 0.5 - -my_str_13 : Str -my_str_13 = "one" - -binops_13 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_13 : U64 -> U64 -add_one_13 = |n| n + 1 - -map_add_one_13 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_13 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_13 = |arg_one, arg_two| arg_one * arg_two - -num_13 = 42 -frac_13 = 4.2 -str_13 = "hello" - -# Polymorphic empty collections -empty_list_13 = [] - -# Mixed polymorphic structures -mixed_13 = { - numbers: { value: num_13, list: [num_13, num_13], float: frac }, - strings: { value: str_13, list: [str_13, str_13] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_13 }, - }, - computations: { - from_num: num_13 * 100, - from_frac: frac_13 * 10.0, - list_from_num: [num_13, num_13, num_13], - }, -} - -x_14 = 3.14 -y_14 = 1.23e45 -z_14 = 0.5 - -my_str_14 : Str -my_str_14 = "one" - -binops_14 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_14 : U64 -> U64 -add_one_14 = |n| n + 1 - -map_add_one_14 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_14 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_14 = |arg_one, arg_two| arg_one * arg_two - -num_14 = 42 -frac_14 = 4.2 -str_14 = "hello" - -# Polymorphic empty collections -empty_list_14 = [] - -# Mixed polymorphic structures -mixed_14 = { - numbers: { value: num_14, list: [num_14, num_14], float: frac }, - strings: { value: str_14, list: [str_14, str_14] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_14 }, - }, - computations: { - from_num: num_14 * 100, - from_frac: frac_14 * 10.0, - list_from_num: [num_14, num_14, num_14], - }, -} - -x_15 = 3.14 -y_15 = 1.23e45 -z_15 = 0.5 - -my_str_15 : Str -my_str_15 = "one" - -binops_15 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_15 : U64 -> U64 -add_one_15 = |n| n + 1 - -map_add_one_15 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_15 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_15 = |arg_one, arg_two| arg_one * arg_two - -num_15 = 42 -frac_15 = 4.2 -str_15 = "hello" - -# Polymorphic empty collections -empty_list_15 = [] - -# Mixed polymorphic structures -mixed_15 = { - numbers: { value: num_15, list: [num_15, num_15], float: frac }, - strings: { value: str_15, list: [str_15, str_15] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_15 }, - }, - computations: { - from_num: num_15 * 100, - from_frac: frac_15 * 10.0, - list_from_num: [num_15, num_15, num_15], - }, -} - -x_16 = 3.14 -y_16 = 1.23e45 -z_16 = 0.5 - -my_str_16 : Str -my_str_16 = "one" - -binops_16 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_16 : U64 -> U64 -add_one_16 = |n| n + 1 - -map_add_one_16 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_16 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_16 = |arg_one, arg_two| arg_one * arg_two - -num_16 = 42 -frac_16 = 4.2 -str_16 = "hello" - -# Polymorphic empty collections -empty_list_16 = [] - -# Mixed polymorphic structures -mixed_16 = { - numbers: { value: num_16, list: [num_16, num_16], float: frac }, - strings: { value: str_16, list: [str_16, str_16] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_16 }, - }, - computations: { - from_num: num_16 * 100, - from_frac: frac_16 * 10.0, - list_from_num: [num_16, num_16, num_16], - }, -} - -x_17 = 3.14 -y_17 = 1.23e45 -z_17 = 0.5 - -my_str_17 : Str -my_str_17 = "one" - -binops_17 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_17 : U64 -> U64 -add_one_17 = |n| n + 1 - -map_add_one_17 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_17 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_17 = |arg_one, arg_two| arg_one * arg_two - -num_17 = 42 -frac_17 = 4.2 -str_17 = "hello" - -# Polymorphic empty collections -empty_list_17 = [] - -# Mixed polymorphic structures -mixed_17 = { - numbers: { value: num_17, list: [num_17, num_17], float: frac }, - strings: { value: str_17, list: [str_17, str_17] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_17 }, - }, - computations: { - from_num: num_17 * 100, - from_frac: frac_17 * 10.0, - list_from_num: [num_17, num_17, num_17], - }, -} - -x_18 = 3.14 -y_18 = 1.23e45 -z_18 = 0.5 - -my_str_18 : Str -my_str_18 = "one" - -binops_18 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_18 : U64 -> U64 -add_one_18 = |n| n + 1 - -map_add_one_18 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_18 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_18 = |arg_one, arg_two| arg_one * arg_two - -num_18 = 42 -frac_18 = 4.2 -str_18 = "hello" - -# Polymorphic empty collections -empty_list_18 = [] - -# Mixed polymorphic structures -mixed_18 = { - numbers: { value: num_18, list: [num_18, num_18], float: frac }, - strings: { value: str_18, list: [str_18, str_18] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_18 }, - }, - computations: { - from_num: num_18 * 100, - from_frac: frac_18 * 10.0, - list_from_num: [num_18, num_18, num_18], - }, -} - -x_19 = 3.14 -y_19 = 1.23e45 -z_19 = 0.5 - -my_str_19 : Str -my_str_19 = "one" - -binops_19 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_19 : U64 -> U64 -add_one_19 = |n| n + 1 - -map_add_one_19 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_19 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_19 = |arg_one, arg_two| arg_one * arg_two - -num_19 = 42 -frac_19 = 4.2 -str_19 = "hello" - -# Polymorphic empty collections -empty_list_19 = [] - -# Mixed polymorphic structures -mixed_19 = { - numbers: { value: num_19, list: [num_19, num_19], float: frac }, - strings: { value: str_19, list: [str_19, str_19] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_19 }, - }, - computations: { - from_num: num_19 * 100, - from_frac: frac_19 * 10.0, - list_from_num: [num_19, num_19, num_19], - }, -} - -x_20 = 3.14 -y_20 = 1.23e45 -z_20 = 0.5 - -my_str_20 : Str -my_str_20 = "one" - -binops_20 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_20 : U64 -> U64 -add_one_20 = |n| n + 1 - -map_add_one_20 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_20 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_20 = |arg_one, arg_two| arg_one * arg_two - -num_20 = 42 -frac_20 = 4.2 -str_20 = "hello" - -# Polymorphic empty collections -empty_list_20 = [] - -# Mixed polymorphic structures -mixed_20 = { - numbers: { value: num_20, list: [num_20, num_20], float: frac }, - strings: { value: str_20, list: [str_20, str_20] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_20 }, - }, - computations: { - from_num: num_20 * 100, - from_frac: frac_20 * 10.0, - list_from_num: [num_20, num_20, num_20], - }, -} - -x_21 = 3.14 -y_21 = 1.23e45 -z_21 = 0.5 - -my_str_21 : Str -my_str_21 = "one" - -binops_21 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_21 : U64 -> U64 -add_one_21 = |n| n + 1 - -map_add_one_21 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_21 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_21 = |arg_one, arg_two| arg_one * arg_two - -num_21 = 42 -frac_21 = 4.2 -str_21 = "hello" - -# Polymorphic empty collections -empty_list_21 = [] - -# Mixed polymorphic structures -mixed_21 = { - numbers: { value: num_21, list: [num_21, num_21], float: frac }, - strings: { value: str_21, list: [str_21, str_21] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_21 }, - }, - computations: { - from_num: num_21 * 100, - from_frac: frac_21 * 10.0, - list_from_num: [num_21, num_21, num_21], - }, -} - -x_22 = 3.14 -y_22 = 1.23e45 -z_22 = 0.5 - -my_str_22 : Str -my_str_22 = "one" - -binops_22 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_22 : U64 -> U64 -add_one_22 = |n| n + 1 - -map_add_one_22 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_22 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_22 = |arg_one, arg_two| arg_one * arg_two - -num_22 = 42 -frac_22 = 4.2 -str_22 = "hello" - -# Polymorphic empty collections -empty_list_22 = [] - -# Mixed polymorphic structures -mixed_22 = { - numbers: { value: num_22, list: [num_22, num_22], float: frac }, - strings: { value: str_22, list: [str_22, str_22] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_22 }, - }, - computations: { - from_num: num_22 * 100, - from_frac: frac_22 * 10.0, - list_from_num: [num_22, num_22, num_22], - }, -} - -x_23 = 3.14 -y_23 = 1.23e45 -z_23 = 0.5 - -my_str_23 : Str -my_str_23 = "one" - -binops_23 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_23 : U64 -> U64 -add_one_23 = |n| n + 1 - -map_add_one_23 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_23 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_23 = |arg_one, arg_two| arg_one * arg_two - -num_23 = 42 -frac_23 = 4.2 -str_23 = "hello" - -# Polymorphic empty collections -empty_list_23 = [] - -# Mixed polymorphic structures -mixed_23 = { - numbers: { value: num_23, list: [num_23, num_23], float: frac }, - strings: { value: str_23, list: [str_23, str_23] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_23 }, - }, - computations: { - from_num: num_23 * 100, - from_frac: frac_23 * 10.0, - list_from_num: [num_23, num_23, num_23], - }, -} - -x_24 = 3.14 -y_24 = 1.23e45 -z_24 = 0.5 - -my_str_24 : Str -my_str_24 = "one" - -binops_24 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_24 : U64 -> U64 -add_one_24 = |n| n + 1 - -map_add_one_24 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_24 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_24 = |arg_one, arg_two| arg_one * arg_two - -num_24 = 42 -frac_24 = 4.2 -str_24 = "hello" - -# Polymorphic empty collections -empty_list_24 = [] - -# Mixed polymorphic structures -mixed_24 = { - numbers: { value: num_24, list: [num_24, num_24], float: frac }, - strings: { value: str_24, list: [str_24, str_24] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_24 }, - }, - computations: { - from_num: num_24 * 100, - from_frac: frac_24 * 10.0, - list_from_num: [num_24, num_24, num_24], - }, -} - -x_25 = 3.14 -y_25 = 1.23e45 -z_25 = 0.5 - -my_str_25 : Str -my_str_25 = "one" - -binops_25 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_25 : U64 -> U64 -add_one_25 = |n| n + 1 - -map_add_one_25 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_25 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_25 = |arg_one, arg_two| arg_one * arg_two - -num_25 = 42 -frac_25 = 4.2 -str_25 = "hello" - -# Polymorphic empty collections -empty_list_25 = [] - -# Mixed polymorphic structures -mixed_25 = { - numbers: { value: num_25, list: [num_25, num_25], float: frac }, - strings: { value: str_25, list: [str_25, str_25] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_25 }, - }, - computations: { - from_num: num_25 * 100, - from_frac: frac_25 * 10.0, - list_from_num: [num_25, num_25, num_25], - }, -} - -x_26 = 3.14 -y_26 = 1.23e45 -z_26 = 0.5 - -my_str_26 : Str -my_str_26 = "one" - -binops_26 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_26 : U64 -> U64 -add_one_26 = |n| n + 1 - -map_add_one_26 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_26 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_26 = |arg_one, arg_two| arg_one * arg_two - -num_26 = 42 -frac_26 = 4.2 -str_26 = "hello" - -# Polymorphic empty collections -empty_list_26 = [] - -# Mixed polymorphic structures -mixed_26 = { - numbers: { value: num_26, list: [num_26, num_26], float: frac }, - strings: { value: str_26, list: [str_26, str_26] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_26 }, - }, - computations: { - from_num: num_26 * 100, - from_frac: frac_26 * 10.0, - list_from_num: [num_26, num_26, num_26], - }, -} - -x_27 = 3.14 -y_27 = 1.23e45 -z_27 = 0.5 - -my_str_27 : Str -my_str_27 = "one" - -binops_27 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_27 : U64 -> U64 -add_one_27 = |n| n + 1 - -map_add_one_27 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_27 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_27 = |arg_one, arg_two| arg_one * arg_two - -num_27 = 42 -frac_27 = 4.2 -str_27 = "hello" - -# Polymorphic empty collections -empty_list_27 = [] - -# Mixed polymorphic structures -mixed_27 = { - numbers: { value: num_27, list: [num_27, num_27], float: frac }, - strings: { value: str_27, list: [str_27, str_27] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_27 }, - }, - computations: { - from_num: num_27 * 100, - from_frac: frac_27 * 10.0, - list_from_num: [num_27, num_27, num_27], - }, -} - -x_28 = 3.14 -y_28 = 1.23e45 -z_28 = 0.5 - -my_str_28 : Str -my_str_28 = "one" - -binops_28 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_28 : U64 -> U64 -add_one_28 = |n| n + 1 - -map_add_one_28 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_28 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_28 = |arg_one, arg_two| arg_one * arg_two - -num_28 = 42 -frac_28 = 4.2 -str_28 = "hello" - -# Polymorphic empty collections -empty_list_28 = [] - -# Mixed polymorphic structures -mixed_28 = { - numbers: { value: num_28, list: [num_28, num_28], float: frac }, - strings: { value: str_28, list: [str_28, str_28] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_28 }, - }, - computations: { - from_num: num_28 * 100, - from_frac: frac_28 * 10.0, - list_from_num: [num_28, num_28, num_28], - }, -} - -x_29 = 3.14 -y_29 = 1.23e45 -z_29 = 0.5 - -my_str_29 : Str -my_str_29 = "one" - -binops_29 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_29 : U64 -> U64 -add_one_29 = |n| n + 1 - -map_add_one_29 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_29 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_29 = |arg_one, arg_two| arg_one * arg_two - -num_29 = 42 -frac_29 = 4.2 -str_29 = "hello" - -# Polymorphic empty collections -empty_list_29 = [] - -# Mixed polymorphic structures -mixed_29 = { - numbers: { value: num_29, list: [num_29, num_29], float: frac }, - strings: { value: str_29, list: [str_29, str_29] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_29 }, - }, - computations: { - from_num: num_29 * 100, - from_frac: frac_29 * 10.0, - list_from_num: [num_29, num_29, num_29], - }, -} - -x_30 = 3.14 -y_30 = 1.23e45 -z_30 = 0.5 - -my_str_30 : Str -my_str_30 = "one" - -binops_30 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_30 : U64 -> U64 -add_one_30 = |n| n + 1 - -map_add_one_30 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_30 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_30 = |arg_one, arg_two| arg_one * arg_two - -num_30 = 42 -frac_30 = 4.2 -str_30 = "hello" - -# Polymorphic empty collections -empty_list_30 = [] - -# Mixed polymorphic structures -mixed_30 = { - numbers: { value: num_30, list: [num_30, num_30], float: frac }, - strings: { value: str_30, list: [str_30, str_30] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_30 }, - }, - computations: { - from_num: num_30 * 100, - from_frac: frac_30 * 10.0, - list_from_num: [num_30, num_30, num_30], - }, -} - -x_31 = 3.14 -y_31 = 1.23e45 -z_31 = 0.5 - -my_str_31 : Str -my_str_31 = "one" - -binops_31 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_31 : U64 -> U64 -add_one_31 = |n| n + 1 - -map_add_one_31 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_31 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_31 = |arg_one, arg_two| arg_one * arg_two - -num_31 = 42 -frac_31 = 4.2 -str_31 = "hello" - -# Polymorphic empty collections -empty_list_31 = [] - -# Mixed polymorphic structures -mixed_31 = { - numbers: { value: num_31, list: [num_31, num_31], float: frac }, - strings: { value: str_31, list: [str_31, str_31] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_31 }, - }, - computations: { - from_num: num_31 * 100, - from_frac: frac_31 * 10.0, - list_from_num: [num_31, num_31, num_31], - }, -} - -x_32 = 3.14 -y_32 = 1.23e45 -z_32 = 0.5 - -my_str_32 : Str -my_str_32 = "one" - -binops_32 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_32 : U64 -> U64 -add_one_32 = |n| n + 1 - -map_add_one_32 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_32 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_32 = |arg_one, arg_two| arg_one * arg_two - -num_32 = 42 -frac_32 = 4.2 -str_32 = "hello" - -# Polymorphic empty collections -empty_list_32 = [] - -# Mixed polymorphic structures -mixed_32 = { - numbers: { value: num_32, list: [num_32, num_32], float: frac }, - strings: { value: str_32, list: [str_32, str_32] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_32 }, - }, - computations: { - from_num: num_32 * 100, - from_frac: frac_32 * 10.0, - list_from_num: [num_32, num_32, num_32], - }, -} - -x_33 = 3.14 -y_33 = 1.23e45 -z_33 = 0.5 - -my_str_33 : Str -my_str_33 = "one" - -binops_33 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_33 : U64 -> U64 -add_one_33 = |n| n + 1 - -map_add_one_33 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_33 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_33 = |arg_one, arg_two| arg_one * arg_two - -num_33 = 42 -frac_33 = 4.2 -str_33 = "hello" - -# Polymorphic empty collections -empty_list_33 = [] - -# Mixed polymorphic structures -mixed_33 = { - numbers: { value: num_33, list: [num_33, num_33], float: frac }, - strings: { value: str_33, list: [str_33, str_33] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_33 }, - }, - computations: { - from_num: num_33 * 100, - from_frac: frac_33 * 10.0, - list_from_num: [num_33, num_33, num_33], - }, -} - -x_34 = 3.14 -y_34 = 1.23e45 -z_34 = 0.5 - -my_str_34 : Str -my_str_34 = "one" - -binops_34 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_34 : U64 -> U64 -add_one_34 = |n| n + 1 - -map_add_one_34 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_34 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_34 = |arg_one, arg_two| arg_one * arg_two - -num_34 = 42 -frac_34 = 4.2 -str_34 = "hello" - -# Polymorphic empty collections -empty_list_34 = [] - -# Mixed polymorphic structures -mixed_34 = { - numbers: { value: num_34, list: [num_34, num_34], float: frac }, - strings: { value: str_34, list: [str_34, str_34] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_34 }, - }, - computations: { - from_num: num_34 * 100, - from_frac: frac_34 * 10.0, - list_from_num: [num_34, num_34, num_34], - }, -} - -x_35 = 3.14 -y_35 = 1.23e45 -z_35 = 0.5 - -my_str_35 : Str -my_str_35 = "one" - -binops_35 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_35 : U64 -> U64 -add_one_35 = |n| n + 1 - -map_add_one_35 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_35 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_35 = |arg_one, arg_two| arg_one * arg_two - -num_35 = 42 -frac_35 = 4.2 -str_35 = "hello" - -# Polymorphic empty collections -empty_list_35 = [] - -# Mixed polymorphic structures -mixed_35 = { - numbers: { value: num_35, list: [num_35, num_35], float: frac }, - strings: { value: str_35, list: [str_35, str_35] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_35 }, - }, - computations: { - from_num: num_35 * 100, - from_frac: frac_35 * 10.0, - list_from_num: [num_35, num_35, num_35], - }, -} - -x_36 = 3.14 -y_36 = 1.23e45 -z_36 = 0.5 - -my_str_36 : Str -my_str_36 = "one" - -binops_36 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_36 : U64 -> U64 -add_one_36 = |n| n + 1 - -map_add_one_36 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_36 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_36 = |arg_one, arg_two| arg_one * arg_two - -num_36 = 42 -frac_36 = 4.2 -str_36 = "hello" - -# Polymorphic empty collections -empty_list_36 = [] - -# Mixed polymorphic structures -mixed_36 = { - numbers: { value: num_36, list: [num_36, num_36], float: frac }, - strings: { value: str_36, list: [str_36, str_36] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_36 }, - }, - computations: { - from_num: num_36 * 100, - from_frac: frac_36 * 10.0, - list_from_num: [num_36, num_36, num_36], - }, -} - -x_37 = 3.14 -y_37 = 1.23e45 -z_37 = 0.5 - -my_str_37 : Str -my_str_37 = "one" - -binops_37 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_37 : U64 -> U64 -add_one_37 = |n| n + 1 - -map_add_one_37 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_37 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_37 = |arg_one, arg_two| arg_one * arg_two - -num_37 = 42 -frac_37 = 4.2 -str_37 = "hello" - -# Polymorphic empty collections -empty_list_37 = [] - -# Mixed polymorphic structures -mixed_37 = { - numbers: { value: num_37, list: [num_37, num_37], float: frac }, - strings: { value: str_37, list: [str_37, str_37] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_37 }, - }, - computations: { - from_num: num_37 * 100, - from_frac: frac_37 * 10.0, - list_from_num: [num_37, num_37, num_37], - }, -} - -x_38 = 3.14 -y_38 = 1.23e45 -z_38 = 0.5 - -my_str_38 : Str -my_str_38 = "one" - -binops_38 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_38 : U64 -> U64 -add_one_38 = |n| n + 1 - -map_add_one_38 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_38 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_38 = |arg_one, arg_two| arg_one * arg_two - -num_38 = 42 -frac_38 = 4.2 -str_38 = "hello" - -# Polymorphic empty collections -empty_list_38 = [] - -# Mixed polymorphic structures -mixed_38 = { - numbers: { value: num_38, list: [num_38, num_38], float: frac }, - strings: { value: str_38, list: [str_38, str_38] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_38 }, - }, - computations: { - from_num: num_38 * 100, - from_frac: frac_38 * 10.0, - list_from_num: [num_38, num_38, num_38], - }, -} - -x_39 = 3.14 -y_39 = 1.23e45 -z_39 = 0.5 - -my_str_39 : Str -my_str_39 = "one" - -binops_39 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_39 : U64 -> U64 -add_one_39 = |n| n + 1 - -map_add_one_39 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_39 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_39 = |arg_one, arg_two| arg_one * arg_two - -num_39 = 42 -frac_39 = 4.2 -str_39 = "hello" - -# Polymorphic empty collections -empty_list_39 = [] - -# Mixed polymorphic structures -mixed_39 = { - numbers: { value: num_39, list: [num_39, num_39], float: frac }, - strings: { value: str_39, list: [str_39, str_39] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_39 }, - }, - computations: { - from_num: num_39 * 100, - from_frac: frac_39 * 10.0, - list_from_num: [num_39, num_39, num_39], - }, -} - -x_40 = 3.14 -y_40 = 1.23e45 -z_40 = 0.5 - -my_str_40 : Str -my_str_40 = "one" - -binops_40 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_40 : U64 -> U64 -add_one_40 = |n| n + 1 - -map_add_one_40 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_40 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_40 = |arg_one, arg_two| arg_one * arg_two - -num_40 = 42 -frac_40 = 4.2 -str_40 = "hello" - -# Polymorphic empty collections -empty_list_40 = [] - -# Mixed polymorphic structures -mixed_40 = { - numbers: { value: num_40, list: [num_40, num_40], float: frac }, - strings: { value: str_40, list: [str_40, str_40] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_40 }, - }, - computations: { - from_num: num_40 * 100, - from_frac: frac_40 * 10.0, - list_from_num: [num_40, num_40, num_40], - }, -} - -x_41 = 3.14 -y_41 = 1.23e45 -z_41 = 0.5 - -my_str_41 : Str -my_str_41 = "one" - -binops_41 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_41 : U64 -> U64 -add_one_41 = |n| n + 1 - -map_add_one_41 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_41 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_41 = |arg_one, arg_two| arg_one * arg_two - -num_41 = 42 -frac_41 = 4.2 -str_41 = "hello" - -# Polymorphic empty collections -empty_list_41 = [] - -# Mixed polymorphic structures -mixed_41 = { - numbers: { value: num_41, list: [num_41, num_41], float: frac }, - strings: { value: str_41, list: [str_41, str_41] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_41 }, - }, - computations: { - from_num: num_41 * 100, - from_frac: frac_41 * 10.0, - list_from_num: [num_41, num_41, num_41], - }, -} - -x_42 = 3.14 -y_42 = 1.23e45 -z_42 = 0.5 - -my_str_42 : Str -my_str_42 = "one" - -binops_42 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_42 : U64 -> U64 -add_one_42 = |n| n + 1 - -map_add_one_42 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_42 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_42 = |arg_one, arg_two| arg_one * arg_two - -num_42 = 42 -frac_42 = 4.2 -str_42 = "hello" - -# Polymorphic empty collections -empty_list_42 = [] - -# Mixed polymorphic structures -mixed_42 = { - numbers: { value: num_42, list: [num_42, num_42], float: frac }, - strings: { value: str_42, list: [str_42, str_42] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_42 }, - }, - computations: { - from_num: num_42 * 100, - from_frac: frac_42 * 10.0, - list_from_num: [num_42, num_42, num_42], - }, -} - -x_43 = 3.14 -y_43 = 1.23e45 -z_43 = 0.5 - -my_str_43 : Str -my_str_43 = "one" - -binops_43 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_43 : U64 -> U64 -add_one_43 = |n| n + 1 - -map_add_one_43 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_43 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_43 = |arg_one, arg_two| arg_one * arg_two - -num_43 = 42 -frac_43 = 4.2 -str_43 = "hello" - -# Polymorphic empty collections -empty_list_43 = [] - -# Mixed polymorphic structures -mixed_43 = { - numbers: { value: num_43, list: [num_43, num_43], float: frac }, - strings: { value: str_43, list: [str_43, str_43] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_43 }, - }, - computations: { - from_num: num_43 * 100, - from_frac: frac_43 * 10.0, - list_from_num: [num_43, num_43, num_43], - }, -} - -x_44 = 3.14 -y_44 = 1.23e45 -z_44 = 0.5 - -my_str_44 : Str -my_str_44 = "one" - -binops_44 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_44 : U64 -> U64 -add_one_44 = |n| n + 1 - -map_add_one_44 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_44 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_44 = |arg_one, arg_two| arg_one * arg_two - -num_44 = 42 -frac_44 = 4.2 -str_44 = "hello" - -# Polymorphic empty collections -empty_list_44 = [] - -# Mixed polymorphic structures -mixed_44 = { - numbers: { value: num_44, list: [num_44, num_44], float: frac }, - strings: { value: str_44, list: [str_44, str_44] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_44 }, - }, - computations: { - from_num: num_44 * 100, - from_frac: frac_44 * 10.0, - list_from_num: [num_44, num_44, num_44], - }, -} - -x_45 = 3.14 -y_45 = 1.23e45 -z_45 = 0.5 - -my_str_45 : Str -my_str_45 = "one" - -binops_45 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_45 : U64 -> U64 -add_one_45 = |n| n + 1 - -map_add_one_45 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_45 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_45 = |arg_one, arg_two| arg_one * arg_two - -num_45 = 42 -frac_45 = 4.2 -str_45 = "hello" - -# Polymorphic empty collections -empty_list_45 = [] - -# Mixed polymorphic structures -mixed_45 = { - numbers: { value: num_45, list: [num_45, num_45], float: frac }, - strings: { value: str_45, list: [str_45, str_45] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_45 }, - }, - computations: { - from_num: num_45 * 100, - from_frac: frac_45 * 10.0, - list_from_num: [num_45, num_45, num_45], - }, -} - -x_46 = 3.14 -y_46 = 1.23e45 -z_46 = 0.5 - -my_str_46 : Str -my_str_46 = "one" - -binops_46 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_46 : U64 -> U64 -add_one_46 = |n| n + 1 - -map_add_one_46 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_46 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_46 = |arg_one, arg_two| arg_one * arg_two - -num_46 = 42 -frac_46 = 4.2 -str_46 = "hello" - -# Polymorphic empty collections -empty_list_46 = [] - -# Mixed polymorphic structures -mixed_46 = { - numbers: { value: num_46, list: [num_46, num_46], float: frac }, - strings: { value: str_46, list: [str_46, str_46] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_46 }, - }, - computations: { - from_num: num_46 * 100, - from_frac: frac_46 * 10.0, - list_from_num: [num_46, num_46, num_46], - }, -} - -x_47 = 3.14 -y_47 = 1.23e45 -z_47 = 0.5 - -my_str_47 : Str -my_str_47 = "one" - -binops_47 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_47 : U64 -> U64 -add_one_47 = |n| n + 1 - -map_add_one_47 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_47 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_47 = |arg_one, arg_two| arg_one * arg_two - -num_47 = 42 -frac_47 = 4.2 -str_47 = "hello" - -# Polymorphic empty collections -empty_list_47 = [] - -# Mixed polymorphic structures -mixed_47 = { - numbers: { value: num_47, list: [num_47, num_47], float: frac }, - strings: { value: str_47, list: [str_47, str_47] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_47 }, - }, - computations: { - from_num: num_47 * 100, - from_frac: frac_47 * 10.0, - list_from_num: [num_47, num_47, num_47], - }, -} - -x_48 = 3.14 -y_48 = 1.23e45 -z_48 = 0.5 - -my_str_48 : Str -my_str_48 = "one" - -binops_48 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_48 : U64 -> U64 -add_one_48 = |n| n + 1 - -map_add_one_48 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_48 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_48 = |arg_one, arg_two| arg_one * arg_two - -num_48 = 42 -frac_48 = 4.2 -str_48 = "hello" - -# Polymorphic empty collections -empty_list_48 = [] - -# Mixed polymorphic structures -mixed_48 = { - numbers: { value: num_48, list: [num_48, num_48], float: frac }, - strings: { value: str_48, list: [str_48, str_48] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_48 }, - }, - computations: { - from_num: num_48 * 100, - from_frac: frac_48 * 10.0, - list_from_num: [num_48, num_48, num_48], - }, -} - -x_49 = 3.14 -y_49 = 1.23e45 -z_49 = 0.5 - -my_str_49 : Str -my_str_49 = "one" - -binops_49 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_49 : U64 -> U64 -add_one_49 = |n| n + 1 - -map_add_one_49 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_49 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_49 = |arg_one, arg_two| arg_one * arg_two - -num_49 = 42 -frac_49 = 4.2 -str_49 = "hello" - -# Polymorphic empty collections -empty_list_49 = [] - -# Mixed polymorphic structures -mixed_49 = { - numbers: { value: num_49, list: [num_49, num_49], float: frac }, - strings: { value: str_49, list: [str_49, str_49] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_49 }, - }, - computations: { - from_num: num_49 * 100, - from_frac: frac_49 * 10.0, - list_from_num: [num_49, num_49, num_49], - }, -} - -x_50 = 3.14 -y_50 = 1.23e45 -z_50 = 0.5 - -my_str_50 : Str -my_str_50 = "one" - -binops_50 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_50 : U64 -> U64 -add_one_50 = |n| n + 1 - -map_add_one_50 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_50 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_50 = |arg_one, arg_two| arg_one * arg_two - -num_50 = 42 -frac_50 = 4.2 -str_50 = "hello" - -# Polymorphic empty collections -empty_list_50 = [] - -# Mixed polymorphic structures -mixed_50 = { - numbers: { value: num_50, list: [num_50, num_50], float: frac }, - strings: { value: str_50, list: [str_50, str_50] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_50 }, - }, - computations: { - from_num: num_50 * 100, - from_frac: frac_50 * 10.0, - list_from_num: [num_50, num_50, num_50], - }, -} - -x_51 = 3.14 -y_51 = 1.23e45 -z_51 = 0.5 - -my_str_51 : Str -my_str_51 = "one" - -binops_51 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_51 : U64 -> U64 -add_one_51 = |n| n + 1 - -map_add_one_51 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_51 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_51 = |arg_one, arg_two| arg_one * arg_two - -num_51 = 42 -frac_51 = 4.2 -str_51 = "hello" - -# Polymorphic empty collections -empty_list_51 = [] - -# Mixed polymorphic structures -mixed_51 = { - numbers: { value: num_51, list: [num_51, num_51], float: frac }, - strings: { value: str_51, list: [str_51, str_51] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_51 }, - }, - computations: { - from_num: num_51 * 100, - from_frac: frac_51 * 10.0, - list_from_num: [num_51, num_51, num_51], - }, -} - -x_52 = 3.14 -y_52 = 1.23e45 -z_52 = 0.5 - -my_str_52 : Str -my_str_52 = "one" - -binops_52 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_52 : U64 -> U64 -add_one_52 = |n| n + 1 - -map_add_one_52 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_52 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_52 = |arg_one, arg_two| arg_one * arg_two - -num_52 = 42 -frac_52 = 4.2 -str_52 = "hello" - -# Polymorphic empty collections -empty_list_52 = [] - -# Mixed polymorphic structures -mixed_52 = { - numbers: { value: num_52, list: [num_52, num_52], float: frac }, - strings: { value: str_52, list: [str_52, str_52] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_52 }, - }, - computations: { - from_num: num_52 * 100, - from_frac: frac_52 * 10.0, - list_from_num: [num_52, num_52, num_52], - }, -} - -x_53 = 3.14 -y_53 = 1.23e45 -z_53 = 0.5 - -my_str_53 : Str -my_str_53 = "one" - -binops_53 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_53 : U64 -> U64 -add_one_53 = |n| n + 1 - -map_add_one_53 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_53 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_53 = |arg_one, arg_two| arg_one * arg_two - -num_53 = 42 -frac_53 = 4.2 -str_53 = "hello" - -# Polymorphic empty collections -empty_list_53 = [] - -# Mixed polymorphic structures -mixed_53 = { - numbers: { value: num_53, list: [num_53, num_53], float: frac }, - strings: { value: str_53, list: [str_53, str_53] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_53 }, - }, - computations: { - from_num: num_53 * 100, - from_frac: frac_53 * 10.0, - list_from_num: [num_53, num_53, num_53], - }, -} - -x_54 = 3.14 -y_54 = 1.23e45 -z_54 = 0.5 - -my_str_54 : Str -my_str_54 = "one" - -binops_54 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_54 : U64 -> U64 -add_one_54 = |n| n + 1 - -map_add_one_54 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_54 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_54 = |arg_one, arg_two| arg_one * arg_two - -num_54 = 42 -frac_54 = 4.2 -str_54 = "hello" - -# Polymorphic empty collections -empty_list_54 = [] - -# Mixed polymorphic structures -mixed_54 = { - numbers: { value: num_54, list: [num_54, num_54], float: frac }, - strings: { value: str_54, list: [str_54, str_54] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_54 }, - }, - computations: { - from_num: num_54 * 100, - from_frac: frac_54 * 10.0, - list_from_num: [num_54, num_54, num_54], - }, -} - -x_55 = 3.14 -y_55 = 1.23e45 -z_55 = 0.5 - -my_str_55 : Str -my_str_55 = "one" - -binops_55 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_55 : U64 -> U64 -add_one_55 = |n| n + 1 - -map_add_one_55 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_55 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_55 = |arg_one, arg_two| arg_one * arg_two - -num_55 = 42 -frac_55 = 4.2 -str_55 = "hello" - -# Polymorphic empty collections -empty_list_55 = [] - -# Mixed polymorphic structures -mixed_55 = { - numbers: { value: num_55, list: [num_55, num_55], float: frac }, - strings: { value: str_55, list: [str_55, str_55] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_55 }, - }, - computations: { - from_num: num_55 * 100, - from_frac: frac_55 * 10.0, - list_from_num: [num_55, num_55, num_55], - }, -} - -x_56 = 3.14 -y_56 = 1.23e45 -z_56 = 0.5 - -my_str_56 : Str -my_str_56 = "one" - -binops_56 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_56 : U64 -> U64 -add_one_56 = |n| n + 1 - -map_add_one_56 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_56 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_56 = |arg_one, arg_two| arg_one * arg_two - -num_56 = 42 -frac_56 = 4.2 -str_56 = "hello" - -# Polymorphic empty collections -empty_list_56 = [] - -# Mixed polymorphic structures -mixed_56 = { - numbers: { value: num_56, list: [num_56, num_56], float: frac }, - strings: { value: str_56, list: [str_56, str_56] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_56 }, - }, - computations: { - from_num: num_56 * 100, - from_frac: frac_56 * 10.0, - list_from_num: [num_56, num_56, num_56], - }, -} - -x_57 = 3.14 -y_57 = 1.23e45 -z_57 = 0.5 - -my_str_57 : Str -my_str_57 = "one" - -binops_57 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_57 : U64 -> U64 -add_one_57 = |n| n + 1 - -map_add_one_57 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_57 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_57 = |arg_one, arg_two| arg_one * arg_two - -num_57 = 42 -frac_57 = 4.2 -str_57 = "hello" - -# Polymorphic empty collections -empty_list_57 = [] - -# Mixed polymorphic structures -mixed_57 = { - numbers: { value: num_57, list: [num_57, num_57], float: frac }, - strings: { value: str_57, list: [str_57, str_57] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_57 }, - }, - computations: { - from_num: num_57 * 100, - from_frac: frac_57 * 10.0, - list_from_num: [num_57, num_57, num_57], - }, -} - -x_58 = 3.14 -y_58 = 1.23e45 -z_58 = 0.5 - -my_str_58 : Str -my_str_58 = "one" - -binops_58 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_58 : U64 -> U64 -add_one_58 = |n| n + 1 - -map_add_one_58 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_58 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_58 = |arg_one, arg_two| arg_one * arg_two - -num_58 = 42 -frac_58 = 4.2 -str_58 = "hello" - -# Polymorphic empty collections -empty_list_58 = [] - -# Mixed polymorphic structures -mixed_58 = { - numbers: { value: num_58, list: [num_58, num_58], float: frac }, - strings: { value: str_58, list: [str_58, str_58] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_58 }, - }, - computations: { - from_num: num_58 * 100, - from_frac: frac_58 * 10.0, - list_from_num: [num_58, num_58, num_58], - }, -} - -x_59 = 3.14 -y_59 = 1.23e45 -z_59 = 0.5 - -my_str_59 : Str -my_str_59 = "one" - -binops_59 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_59 : U64 -> U64 -add_one_59 = |n| n + 1 - -map_add_one_59 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_59 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_59 = |arg_one, arg_two| arg_one * arg_two - -num_59 = 42 -frac_59 = 4.2 -str_59 = "hello" - -# Polymorphic empty collections -empty_list_59 = [] - -# Mixed polymorphic structures -mixed_59 = { - numbers: { value: num_59, list: [num_59, num_59], float: frac }, - strings: { value: str_59, list: [str_59, str_59] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_59 }, - }, - computations: { - from_num: num_59 * 100, - from_frac: frac_59 * 10.0, - list_from_num: [num_59, num_59, num_59], - }, -} - -x_60 = 3.14 -y_60 = 1.23e45 -z_60 = 0.5 - -my_str_60 : Str -my_str_60 = "one" - -binops_60 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_60 : U64 -> U64 -add_one_60 = |n| n + 1 - -map_add_one_60 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_60 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_60 = |arg_one, arg_two| arg_one * arg_two - -num_60 = 42 -frac_60 = 4.2 -str_60 = "hello" - -# Polymorphic empty collections -empty_list_60 = [] - -# Mixed polymorphic structures -mixed_60 = { - numbers: { value: num_60, list: [num_60, num_60], float: frac }, - strings: { value: str_60, list: [str_60, str_60] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_60 }, - }, - computations: { - from_num: num_60 * 100, - from_frac: frac_60 * 10.0, - list_from_num: [num_60, num_60, num_60], - }, -} - -x_61 = 3.14 -y_61 = 1.23e45 -z_61 = 0.5 - -my_str_61 : Str -my_str_61 = "one" - -binops_61 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_61 : U64 -> U64 -add_one_61 = |n| n + 1 - -map_add_one_61 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_61 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_61 = |arg_one, arg_two| arg_one * arg_two - -num_61 = 42 -frac_61 = 4.2 -str_61 = "hello" - -# Polymorphic empty collections -empty_list_61 = [] - -# Mixed polymorphic structures -mixed_61 = { - numbers: { value: num_61, list: [num_61, num_61], float: frac }, - strings: { value: str_61, list: [str_61, str_61] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_61 }, - }, - computations: { - from_num: num_61 * 100, - from_frac: frac_61 * 10.0, - list_from_num: [num_61, num_61, num_61], - }, -} - -x_62 = 3.14 -y_62 = 1.23e45 -z_62 = 0.5 - -my_str_62 : Str -my_str_62 = "one" - -binops_62 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_62 : U64 -> U64 -add_one_62 = |n| n + 1 - -map_add_one_62 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_62 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_62 = |arg_one, arg_two| arg_one * arg_two - -num_62 = 42 -frac_62 = 4.2 -str_62 = "hello" - -# Polymorphic empty collections -empty_list_62 = [] - -# Mixed polymorphic structures -mixed_62 = { - numbers: { value: num_62, list: [num_62, num_62], float: frac }, - strings: { value: str_62, list: [str_62, str_62] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_62 }, - }, - computations: { - from_num: num_62 * 100, - from_frac: frac_62 * 10.0, - list_from_num: [num_62, num_62, num_62], - }, -} - -x_63 = 3.14 -y_63 = 1.23e45 -z_63 = 0.5 - -my_str_63 : Str -my_str_63 = "one" - -binops_63 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_63 : U64 -> U64 -add_one_63 = |n| n + 1 - -map_add_one_63 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_63 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_63 = |arg_one, arg_two| arg_one * arg_two - -num_63 = 42 -frac_63 = 4.2 -str_63 = "hello" - -# Polymorphic empty collections -empty_list_63 = [] - -# Mixed polymorphic structures -mixed_63 = { - numbers: { value: num_63, list: [num_63, num_63], float: frac }, - strings: { value: str_63, list: [str_63, str_63] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_63 }, - }, - computations: { - from_num: num_63 * 100, - from_frac: frac_63 * 10.0, - list_from_num: [num_63, num_63, num_63], - }, -} - -x_64 = 3.14 -y_64 = 1.23e45 -z_64 = 0.5 - -my_str_64 : Str -my_str_64 = "one" - -binops_64 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_64 : U64 -> U64 -add_one_64 = |n| n + 1 - -map_add_one_64 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_64 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_64 = |arg_one, arg_two| arg_one * arg_two - -num_64 = 42 -frac_64 = 4.2 -str_64 = "hello" - -# Polymorphic empty collections -empty_list_64 = [] - -# Mixed polymorphic structures -mixed_64 = { - numbers: { value: num_64, list: [num_64, num_64], float: frac }, - strings: { value: str_64, list: [str_64, str_64] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_64 }, - }, - computations: { - from_num: num_64 * 100, - from_frac: frac_64 * 10.0, - list_from_num: [num_64, num_64, num_64], - }, -} - -x_65 = 3.14 -y_65 = 1.23e45 -z_65 = 0.5 - -my_str_65 : Str -my_str_65 = "one" - -binops_65 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_65 : U64 -> U64 -add_one_65 = |n| n + 1 - -map_add_one_65 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_65 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_65 = |arg_one, arg_two| arg_one * arg_two - -num_65 = 42 -frac_65 = 4.2 -str_65 = "hello" - -# Polymorphic empty collections -empty_list_65 = [] - -# Mixed polymorphic structures -mixed_65 = { - numbers: { value: num_65, list: [num_65, num_65], float: frac }, - strings: { value: str_65, list: [str_65, str_65] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_65 }, - }, - computations: { - from_num: num_65 * 100, - from_frac: frac_65 * 10.0, - list_from_num: [num_65, num_65, num_65], - }, -} - -x_66 = 3.14 -y_66 = 1.23e45 -z_66 = 0.5 - -my_str_66 : Str -my_str_66 = "one" - -binops_66 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_66 : U64 -> U64 -add_one_66 = |n| n + 1 - -map_add_one_66 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_66 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_66 = |arg_one, arg_two| arg_one * arg_two - -num_66 = 42 -frac_66 = 4.2 -str_66 = "hello" - -# Polymorphic empty collections -empty_list_66 = [] - -# Mixed polymorphic structures -mixed_66 = { - numbers: { value: num_66, list: [num_66, num_66], float: frac }, - strings: { value: str_66, list: [str_66, str_66] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_66 }, - }, - computations: { - from_num: num_66 * 100, - from_frac: frac_66 * 10.0, - list_from_num: [num_66, num_66, num_66], - }, -} - -x_67 = 3.14 -y_67 = 1.23e45 -z_67 = 0.5 - -my_str_67 : Str -my_str_67 = "one" - -binops_67 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_67 : U64 -> U64 -add_one_67 = |n| n + 1 - -map_add_one_67 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_67 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_67 = |arg_one, arg_two| arg_one * arg_two - -num_67 = 42 -frac_67 = 4.2 -str_67 = "hello" - -# Polymorphic empty collections -empty_list_67 = [] - -# Mixed polymorphic structures -mixed_67 = { - numbers: { value: num_67, list: [num_67, num_67], float: frac }, - strings: { value: str_67, list: [str_67, str_67] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_67 }, - }, - computations: { - from_num: num_67 * 100, - from_frac: frac_67 * 10.0, - list_from_num: [num_67, num_67, num_67], - }, -} - -x_68 = 3.14 -y_68 = 1.23e45 -z_68 = 0.5 - -my_str_68 : Str -my_str_68 = "one" - -binops_68 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_68 : U64 -> U64 -add_one_68 = |n| n + 1 - -map_add_one_68 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_68 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_68 = |arg_one, arg_two| arg_one * arg_two - -num_68 = 42 -frac_68 = 4.2 -str_68 = "hello" - -# Polymorphic empty collections -empty_list_68 = [] - -# Mixed polymorphic structures -mixed_68 = { - numbers: { value: num_68, list: [num_68, num_68], float: frac }, - strings: { value: str_68, list: [str_68, str_68] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_68 }, - }, - computations: { - from_num: num_68 * 100, - from_frac: frac_68 * 10.0, - list_from_num: [num_68, num_68, num_68], - }, -} - -x_69 = 3.14 -y_69 = 1.23e45 -z_69 = 0.5 - -my_str_69 : Str -my_str_69 = "one" - -binops_69 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_69 : U64 -> U64 -add_one_69 = |n| n + 1 - -map_add_one_69 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_69 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_69 = |arg_one, arg_two| arg_one * arg_two - -num_69 = 42 -frac_69 = 4.2 -str_69 = "hello" - -# Polymorphic empty collections -empty_list_69 = [] - -# Mixed polymorphic structures -mixed_69 = { - numbers: { value: num_69, list: [num_69, num_69], float: frac }, - strings: { value: str_69, list: [str_69, str_69] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_69 }, - }, - computations: { - from_num: num_69 * 100, - from_frac: frac_69 * 10.0, - list_from_num: [num_69, num_69, num_69], - }, -} - -x_70 = 3.14 -y_70 = 1.23e45 -z_70 = 0.5 - -my_str_70 : Str -my_str_70 = "one" - -binops_70 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_70 : U64 -> U64 -add_one_70 = |n| n + 1 - -map_add_one_70 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_70 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_70 = |arg_one, arg_two| arg_one * arg_two - -num_70 = 42 -frac_70 = 4.2 -str_70 = "hello" - -# Polymorphic empty collections -empty_list_70 = [] - -# Mixed polymorphic structures -mixed_70 = { - numbers: { value: num_70, list: [num_70, num_70], float: frac }, - strings: { value: str_70, list: [str_70, str_70] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_70 }, - }, - computations: { - from_num: num_70 * 100, - from_frac: frac_70 * 10.0, - list_from_num: [num_70, num_70, num_70], - }, -} - -x_71 = 3.14 -y_71 = 1.23e45 -z_71 = 0.5 - -my_str_71 : Str -my_str_71 = "one" - -binops_71 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_71 : U64 -> U64 -add_one_71 = |n| n + 1 - -map_add_one_71 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_71 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_71 = |arg_one, arg_two| arg_one * arg_two - -num_71 = 42 -frac_71 = 4.2 -str_71 = "hello" - -# Polymorphic empty collections -empty_list_71 = [] - -# Mixed polymorphic structures -mixed_71 = { - numbers: { value: num_71, list: [num_71, num_71], float: frac }, - strings: { value: str_71, list: [str_71, str_71] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_71 }, - }, - computations: { - from_num: num_71 * 100, - from_frac: frac_71 * 10.0, - list_from_num: [num_71, num_71, num_71], - }, -} - -x_72 = 3.14 -y_72 = 1.23e45 -z_72 = 0.5 - -my_str_72 : Str -my_str_72 = "one" - -binops_72 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_72 : U64 -> U64 -add_one_72 = |n| n + 1 - -map_add_one_72 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_72 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_72 = |arg_one, arg_two| arg_one * arg_two - -num_72 = 42 -frac_72 = 4.2 -str_72 = "hello" - -# Polymorphic empty collections -empty_list_72 = [] - -# Mixed polymorphic structures -mixed_72 = { - numbers: { value: num_72, list: [num_72, num_72], float: frac }, - strings: { value: str_72, list: [str_72, str_72] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_72 }, - }, - computations: { - from_num: num_72 * 100, - from_frac: frac_72 * 10.0, - list_from_num: [num_72, num_72, num_72], - }, -} - -x_73 = 3.14 -y_73 = 1.23e45 -z_73 = 0.5 - -my_str_73 : Str -my_str_73 = "one" - -binops_73 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_73 : U64 -> U64 -add_one_73 = |n| n + 1 - -map_add_one_73 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_73 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_73 = |arg_one, arg_two| arg_one * arg_two - -num_73 = 42 -frac_73 = 4.2 -str_73 = "hello" - -# Polymorphic empty collections -empty_list_73 = [] - -# Mixed polymorphic structures -mixed_73 = { - numbers: { value: num_73, list: [num_73, num_73], float: frac }, - strings: { value: str_73, list: [str_73, str_73] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_73 }, - }, - computations: { - from_num: num_73 * 100, - from_frac: frac_73 * 10.0, - list_from_num: [num_73, num_73, num_73], - }, -} - -x_74 = 3.14 -y_74 = 1.23e45 -z_74 = 0.5 - -my_str_74 : Str -my_str_74 = "one" - -binops_74 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_74 : U64 -> U64 -add_one_74 = |n| n + 1 - -map_add_one_74 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_74 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_74 = |arg_one, arg_two| arg_one * arg_two - -num_74 = 42 -frac_74 = 4.2 -str_74 = "hello" - -# Polymorphic empty collections -empty_list_74 = [] - -# Mixed polymorphic structures -mixed_74 = { - numbers: { value: num_74, list: [num_74, num_74], float: frac }, - strings: { value: str_74, list: [str_74, str_74] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_74 }, - }, - computations: { - from_num: num_74 * 100, - from_frac: frac_74 * 10.0, - list_from_num: [num_74, num_74, num_74], - }, -} - -x_75 = 3.14 -y_75 = 1.23e45 -z_75 = 0.5 - -my_str_75 : Str -my_str_75 = "one" - -binops_75 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_75 : U64 -> U64 -add_one_75 = |n| n + 1 - -map_add_one_75 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_75 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_75 = |arg_one, arg_two| arg_one * arg_two - -num_75 = 42 -frac_75 = 4.2 -str_75 = "hello" - -# Polymorphic empty collections -empty_list_75 = [] - -# Mixed polymorphic structures -mixed_75 = { - numbers: { value: num_75, list: [num_75, num_75], float: frac }, - strings: { value: str_75, list: [str_75, str_75] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_75 }, - }, - computations: { - from_num: num_75 * 100, - from_frac: frac_75 * 10.0, - list_from_num: [num_75, num_75, num_75], - }, -} - -x_76 = 3.14 -y_76 = 1.23e45 -z_76 = 0.5 - -my_str_76 : Str -my_str_76 = "one" - -binops_76 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_76 : U64 -> U64 -add_one_76 = |n| n + 1 - -map_add_one_76 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_76 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_76 = |arg_one, arg_two| arg_one * arg_two - -num_76 = 42 -frac_76 = 4.2 -str_76 = "hello" - -# Polymorphic empty collections -empty_list_76 = [] - -# Mixed polymorphic structures -mixed_76 = { - numbers: { value: num_76, list: [num_76, num_76], float: frac }, - strings: { value: str_76, list: [str_76, str_76] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_76 }, - }, - computations: { - from_num: num_76 * 100, - from_frac: frac_76 * 10.0, - list_from_num: [num_76, num_76, num_76], - }, -} - -x_77 = 3.14 -y_77 = 1.23e45 -z_77 = 0.5 - -my_str_77 : Str -my_str_77 = "one" - -binops_77 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_77 : U64 -> U64 -add_one_77 = |n| n + 1 - -map_add_one_77 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_77 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_77 = |arg_one, arg_two| arg_one * arg_two - -num_77 = 42 -frac_77 = 4.2 -str_77 = "hello" - -# Polymorphic empty collections -empty_list_77 = [] - -# Mixed polymorphic structures -mixed_77 = { - numbers: { value: num_77, list: [num_77, num_77], float: frac }, - strings: { value: str_77, list: [str_77, str_77] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_77 }, - }, - computations: { - from_num: num_77 * 100, - from_frac: frac_77 * 10.0, - list_from_num: [num_77, num_77, num_77], - }, -} - -x_78 = 3.14 -y_78 = 1.23e45 -z_78 = 0.5 - -my_str_78 : Str -my_str_78 = "one" - -binops_78 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_78 : U64 -> U64 -add_one_78 = |n| n + 1 - -map_add_one_78 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_78 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_78 = |arg_one, arg_two| arg_one * arg_two - -num_78 = 42 -frac_78 = 4.2 -str_78 = "hello" - -# Polymorphic empty collections -empty_list_78 = [] - -# Mixed polymorphic structures -mixed_78 = { - numbers: { value: num_78, list: [num_78, num_78], float: frac }, - strings: { value: str_78, list: [str_78, str_78] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_78 }, - }, - computations: { - from_num: num_78 * 100, - from_frac: frac_78 * 10.0, - list_from_num: [num_78, num_78, num_78], - }, -} - -x_79 = 3.14 -y_79 = 1.23e45 -z_79 = 0.5 - -my_str_79 : Str -my_str_79 = "one" - -binops_79 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_79 : U64 -> U64 -add_one_79 = |n| n + 1 - -map_add_one_79 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_79 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_79 = |arg_one, arg_two| arg_one * arg_two - -num_79 = 42 -frac_79 = 4.2 -str_79 = "hello" - -# Polymorphic empty collections -empty_list_79 = [] - -# Mixed polymorphic structures -mixed_79 = { - numbers: { value: num_79, list: [num_79, num_79], float: frac }, - strings: { value: str_79, list: [str_79, str_79] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_79 }, - }, - computations: { - from_num: num_79 * 100, - from_frac: frac_79 * 10.0, - list_from_num: [num_79, num_79, num_79], - }, -} - -x_80 = 3.14 -y_80 = 1.23e45 -z_80 = 0.5 - -my_str_80 : Str -my_str_80 = "one" - -binops_80 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_80 : U64 -> U64 -add_one_80 = |n| n + 1 - -map_add_one_80 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_80 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_80 = |arg_one, arg_two| arg_one * arg_two - -num_80 = 42 -frac_80 = 4.2 -str_80 = "hello" - -# Polymorphic empty collections -empty_list_80 = [] - -# Mixed polymorphic structures -mixed_80 = { - numbers: { value: num_80, list: [num_80, num_80], float: frac }, - strings: { value: str_80, list: [str_80, str_80] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_80 }, - }, - computations: { - from_num: num_80 * 100, - from_frac: frac_80 * 10.0, - list_from_num: [num_80, num_80, num_80], - }, -} - -x_81 = 3.14 -y_81 = 1.23e45 -z_81 = 0.5 - -my_str_81 : Str -my_str_81 = "one" - -binops_81 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_81 : U64 -> U64 -add_one_81 = |n| n + 1 - -map_add_one_81 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_81 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_81 = |arg_one, arg_two| arg_one * arg_two - -num_81 = 42 -frac_81 = 4.2 -str_81 = "hello" - -# Polymorphic empty collections -empty_list_81 = [] - -# Mixed polymorphic structures -mixed_81 = { - numbers: { value: num_81, list: [num_81, num_81], float: frac }, - strings: { value: str_81, list: [str_81, str_81] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_81 }, - }, - computations: { - from_num: num_81 * 100, - from_frac: frac_81 * 10.0, - list_from_num: [num_81, num_81, num_81], - }, -} - -x_82 = 3.14 -y_82 = 1.23e45 -z_82 = 0.5 - -my_str_82 : Str -my_str_82 = "one" - -binops_82 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_82 : U64 -> U64 -add_one_82 = |n| n + 1 - -map_add_one_82 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_82 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_82 = |arg_one, arg_two| arg_one * arg_two - -num_82 = 42 -frac_82 = 4.2 -str_82 = "hello" - -# Polymorphic empty collections -empty_list_82 = [] - -# Mixed polymorphic structures -mixed_82 = { - numbers: { value: num_82, list: [num_82, num_82], float: frac }, - strings: { value: str_82, list: [str_82, str_82] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_82 }, - }, - computations: { - from_num: num_82 * 100, - from_frac: frac_82 * 10.0, - list_from_num: [num_82, num_82, num_82], - }, -} - -x_83 = 3.14 -y_83 = 1.23e45 -z_83 = 0.5 - -my_str_83 : Str -my_str_83 = "one" - -binops_83 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_83 : U64 -> U64 -add_one_83 = |n| n + 1 - -map_add_one_83 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_83 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_83 = |arg_one, arg_two| arg_one * arg_two - -num_83 = 42 -frac_83 = 4.2 -str_83 = "hello" - -# Polymorphic empty collections -empty_list_83 = [] - -# Mixed polymorphic structures -mixed_83 = { - numbers: { value: num_83, list: [num_83, num_83], float: frac }, - strings: { value: str_83, list: [str_83, str_83] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_83 }, - }, - computations: { - from_num: num_83 * 100, - from_frac: frac_83 * 10.0, - list_from_num: [num_83, num_83, num_83], - }, -} - -x_84 = 3.14 -y_84 = 1.23e45 -z_84 = 0.5 - -my_str_84 : Str -my_str_84 = "one" - -binops_84 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_84 : U64 -> U64 -add_one_84 = |n| n + 1 - -map_add_one_84 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_84 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_84 = |arg_one, arg_two| arg_one * arg_two - -num_84 = 42 -frac_84 = 4.2 -str_84 = "hello" - -# Polymorphic empty collections -empty_list_84 = [] - -# Mixed polymorphic structures -mixed_84 = { - numbers: { value: num_84, list: [num_84, num_84], float: frac }, - strings: { value: str_84, list: [str_84, str_84] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_84 }, - }, - computations: { - from_num: num_84 * 100, - from_frac: frac_84 * 10.0, - list_from_num: [num_84, num_84, num_84], - }, -} - -x_85 = 3.14 -y_85 = 1.23e45 -z_85 = 0.5 - -my_str_85 : Str -my_str_85 = "one" - -binops_85 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_85 : U64 -> U64 -add_one_85 = |n| n + 1 - -map_add_one_85 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_85 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_85 = |arg_one, arg_two| arg_one * arg_two - -num_85 = 42 -frac_85 = 4.2 -str_85 = "hello" - -# Polymorphic empty collections -empty_list_85 = [] - -# Mixed polymorphic structures -mixed_85 = { - numbers: { value: num_85, list: [num_85, num_85], float: frac }, - strings: { value: str_85, list: [str_85, str_85] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_85 }, - }, - computations: { - from_num: num_85 * 100, - from_frac: frac_85 * 10.0, - list_from_num: [num_85, num_85, num_85], - }, -} - -x_86 = 3.14 -y_86 = 1.23e45 -z_86 = 0.5 - -my_str_86 : Str -my_str_86 = "one" - -binops_86 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_86 : U64 -> U64 -add_one_86 = |n| n + 1 - -map_add_one_86 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_86 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_86 = |arg_one, arg_two| arg_one * arg_two - -num_86 = 42 -frac_86 = 4.2 -str_86 = "hello" - -# Polymorphic empty collections -empty_list_86 = [] - -# Mixed polymorphic structures -mixed_86 = { - numbers: { value: num_86, list: [num_86, num_86], float: frac }, - strings: { value: str_86, list: [str_86, str_86] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_86 }, - }, - computations: { - from_num: num_86 * 100, - from_frac: frac_86 * 10.0, - list_from_num: [num_86, num_86, num_86], - }, -} - -x_87 = 3.14 -y_87 = 1.23e45 -z_87 = 0.5 - -my_str_87 : Str -my_str_87 = "one" - -binops_87 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_87 : U64 -> U64 -add_one_87 = |n| n + 1 - -map_add_one_87 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_87 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_87 = |arg_one, arg_two| arg_one * arg_two - -num_87 = 42 -frac_87 = 4.2 -str_87 = "hello" - -# Polymorphic empty collections -empty_list_87 = [] - -# Mixed polymorphic structures -mixed_87 = { - numbers: { value: num_87, list: [num_87, num_87], float: frac }, - strings: { value: str_87, list: [str_87, str_87] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_87 }, - }, - computations: { - from_num: num_87 * 100, - from_frac: frac_87 * 10.0, - list_from_num: [num_87, num_87, num_87], - }, -} - -x_88 = 3.14 -y_88 = 1.23e45 -z_88 = 0.5 - -my_str_88 : Str -my_str_88 = "one" - -binops_88 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_88 : U64 -> U64 -add_one_88 = |n| n + 1 - -map_add_one_88 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_88 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_88 = |arg_one, arg_two| arg_one * arg_two - -num_88 = 42 -frac_88 = 4.2 -str_88 = "hello" - -# Polymorphic empty collections -empty_list_88 = [] - -# Mixed polymorphic structures -mixed_88 = { - numbers: { value: num_88, list: [num_88, num_88], float: frac }, - strings: { value: str_88, list: [str_88, str_88] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_88 }, - }, - computations: { - from_num: num_88 * 100, - from_frac: frac_88 * 10.0, - list_from_num: [num_88, num_88, num_88], - }, -} - -x_89 = 3.14 -y_89 = 1.23e45 -z_89 = 0.5 - -my_str_89 : Str -my_str_89 = "one" - -binops_89 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_89 : U64 -> U64 -add_one_89 = |n| n + 1 - -map_add_one_89 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_89 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_89 = |arg_one, arg_two| arg_one * arg_two - -num_89 = 42 -frac_89 = 4.2 -str_89 = "hello" - -# Polymorphic empty collections -empty_list_89 = [] - -# Mixed polymorphic structures -mixed_89 = { - numbers: { value: num_89, list: [num_89, num_89], float: frac }, - strings: { value: str_89, list: [str_89, str_89] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_89 }, - }, - computations: { - from_num: num_89 * 100, - from_frac: frac_89 * 10.0, - list_from_num: [num_89, num_89, num_89], - }, -} - -x_90 = 3.14 -y_90 = 1.23e45 -z_90 = 0.5 - -my_str_90 : Str -my_str_90 = "one" - -binops_90 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_90 : U64 -> U64 -add_one_90 = |n| n + 1 - -map_add_one_90 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_90 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_90 = |arg_one, arg_two| arg_one * arg_two - -num_90 = 42 -frac_90 = 4.2 -str_90 = "hello" - -# Polymorphic empty collections -empty_list_90 = [] - -# Mixed polymorphic structures -mixed_90 = { - numbers: { value: num_90, list: [num_90, num_90], float: frac }, - strings: { value: str_90, list: [str_90, str_90] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_90 }, - }, - computations: { - from_num: num_90 * 100, - from_frac: frac_90 * 10.0, - list_from_num: [num_90, num_90, num_90], - }, -} - -x_91 = 3.14 -y_91 = 1.23e45 -z_91 = 0.5 - -my_str_91 : Str -my_str_91 = "one" - -binops_91 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_91 : U64 -> U64 -add_one_91 = |n| n + 1 - -map_add_one_91 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_91 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_91 = |arg_one, arg_two| arg_one * arg_two - -num_91 = 42 -frac_91 = 4.2 -str_91 = "hello" - -# Polymorphic empty collections -empty_list_91 = [] - -# Mixed polymorphic structures -mixed_91 = { - numbers: { value: num_91, list: [num_91, num_91], float: frac }, - strings: { value: str_91, list: [str_91, str_91] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_91 }, - }, - computations: { - from_num: num_91 * 100, - from_frac: frac_91 * 10.0, - list_from_num: [num_91, num_91, num_91], - }, -} - -x_92 = 3.14 -y_92 = 1.23e45 -z_92 = 0.5 - -my_str_92 : Str -my_str_92 = "one" - -binops_92 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_92 : U64 -> U64 -add_one_92 = |n| n + 1 - -map_add_one_92 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_92 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_92 = |arg_one, arg_two| arg_one * arg_two - -num_92 = 42 -frac_92 = 4.2 -str_92 = "hello" - -# Polymorphic empty collections -empty_list_92 = [] - -# Mixed polymorphic structures -mixed_92 = { - numbers: { value: num_92, list: [num_92, num_92], float: frac }, - strings: { value: str_92, list: [str_92, str_92] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_92 }, - }, - computations: { - from_num: num_92 * 100, - from_frac: frac_92 * 10.0, - list_from_num: [num_92, num_92, num_92], - }, -} - -x_93 = 3.14 -y_93 = 1.23e45 -z_93 = 0.5 - -my_str_93 : Str -my_str_93 = "one" - -binops_93 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_93 : U64 -> U64 -add_one_93 = |n| n + 1 - -map_add_one_93 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_93 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_93 = |arg_one, arg_two| arg_one * arg_two - -num_93 = 42 -frac_93 = 4.2 -str_93 = "hello" - -# Polymorphic empty collections -empty_list_93 = [] - -# Mixed polymorphic structures -mixed_93 = { - numbers: { value: num_93, list: [num_93, num_93], float: frac }, - strings: { value: str_93, list: [str_93, str_93] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_93 }, - }, - computations: { - from_num: num_93 * 100, - from_frac: frac_93 * 10.0, - list_from_num: [num_93, num_93, num_93], - }, -} - -x_94 = 3.14 -y_94 = 1.23e45 -z_94 = 0.5 - -my_str_94 : Str -my_str_94 = "one" - -binops_94 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_94 : U64 -> U64 -add_one_94 = |n| n + 1 - -map_add_one_94 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_94 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_94 = |arg_one, arg_two| arg_one * arg_two - -num_94 = 42 -frac_94 = 4.2 -str_94 = "hello" - -# Polymorphic empty collections -empty_list_94 = [] - -# Mixed polymorphic structures -mixed_94 = { - numbers: { value: num_94, list: [num_94, num_94], float: frac }, - strings: { value: str_94, list: [str_94, str_94] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_94 }, - }, - computations: { - from_num: num_94 * 100, - from_frac: frac_94 * 10.0, - list_from_num: [num_94, num_94, num_94], - }, -} - -x_95 = 3.14 -y_95 = 1.23e45 -z_95 = 0.5 - -my_str_95 : Str -my_str_95 = "one" - -binops_95 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_95 : U64 -> U64 -add_one_95 = |n| n + 1 - -map_add_one_95 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_95 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_95 = |arg_one, arg_two| arg_one * arg_two - -num_95 = 42 -frac_95 = 4.2 -str_95 = "hello" - -# Polymorphic empty collections -empty_list_95 = [] - -# Mixed polymorphic structures -mixed_95 = { - numbers: { value: num_95, list: [num_95, num_95], float: frac }, - strings: { value: str_95, list: [str_95, str_95] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_95 }, - }, - computations: { - from_num: num_95 * 100, - from_frac: frac_95 * 10.0, - list_from_num: [num_95, num_95, num_95], - }, -} - -x_96 = 3.14 -y_96 = 1.23e45 -z_96 = 0.5 - -my_str_96 : Str -my_str_96 = "one" - -binops_96 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_96 : U64 -> U64 -add_one_96 = |n| n + 1 - -map_add_one_96 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_96 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_96 = |arg_one, arg_two| arg_one * arg_two - -num_96 = 42 -frac_96 = 4.2 -str_96 = "hello" - -# Polymorphic empty collections -empty_list_96 = [] - -# Mixed polymorphic structures -mixed_96 = { - numbers: { value: num_96, list: [num_96, num_96], float: frac }, - strings: { value: str_96, list: [str_96, str_96] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_96 }, - }, - computations: { - from_num: num_96 * 100, - from_frac: frac_96 * 10.0, - list_from_num: [num_96, num_96, num_96], - }, -} - -x_97 = 3.14 -y_97 = 1.23e45 -z_97 = 0.5 - -my_str_97 : Str -my_str_97 = "one" - -binops_97 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_97 : U64 -> U64 -add_one_97 = |n| n + 1 - -map_add_one_97 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_97 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_97 = |arg_one, arg_two| arg_one * arg_two - -num_97 = 42 -frac_97 = 4.2 -str_97 = "hello" - -# Polymorphic empty collections -empty_list_97 = [] - -# Mixed polymorphic structures -mixed_97 = { - numbers: { value: num_97, list: [num_97, num_97], float: frac }, - strings: { value: str_97, list: [str_97, str_97] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_97 }, - }, - computations: { - from_num: num_97 * 100, - from_frac: frac_97 * 10.0, - list_from_num: [num_97, num_97, num_97], - }, -} - -x_98 = 3.14 -y_98 = 1.23e45 -z_98 = 0.5 - -my_str_98 : Str -my_str_98 = "one" - -binops_98 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_98 : U64 -> U64 -add_one_98 = |n| n + 1 - -map_add_one_98 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_98 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_98 = |arg_one, arg_two| arg_one * arg_two - -num_98 = 42 -frac_98 = 4.2 -str_98 = "hello" - -# Polymorphic empty collections -empty_list_98 = [] - -# Mixed polymorphic structures -mixed_98 = { - numbers: { value: num_98, list: [num_98, num_98], float: frac }, - strings: { value: str_98, list: [str_98, str_98] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_98 }, - }, - computations: { - from_num: num_98 * 100, - from_frac: frac_98 * 10.0, - list_from_num: [num_98, num_98, num_98], - }, -} - -x_99 = 3.14 -y_99 = 1.23e45 -z_99 = 0.5 - -my_str_99 : Str -my_str_99 = "one" - -binops_99 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_99 : U64 -> U64 -add_one_99 = |n| n + 1 - -map_add_one_99 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_99 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_99 = |arg_one, arg_two| arg_one * arg_two - -num_99 = 42 -frac_99 = 4.2 -str_99 = "hello" - -# Polymorphic empty collections -empty_list_99 = [] - -# Mixed polymorphic structures -mixed_99 = { - numbers: { value: num_99, list: [num_99, num_99], float: frac }, - strings: { value: str_99, list: [str_99, str_99] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_99 }, - }, - computations: { - from_num: num_99 * 100, - from_frac: frac_99 * 10.0, - list_from_num: [num_99, num_99, num_99], - }, -} - -x_100 = 3.14 -y_100 = 1.23e45 -z_100 = 0.5 - -my_str_100 : Str -my_str_100 = "one" - -binops_100 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_100 : U64 -> U64 -add_one_100 = |n| n + 1 - -map_add_one_100 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_100 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_100 = |arg_one, arg_two| arg_one * arg_two - -num_100 = 42 -frac_100 = 4.2 -str_100 = "hello" - -# Polymorphic empty collections -empty_list_100 = [] - -# Mixed polymorphic structures -mixed_100 = { - numbers: { value: num_100, list: [num_100, num_100], float: frac }, - strings: { value: str_100, list: [str_100, str_100] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_100 }, - }, - computations: { - from_num: num_100 * 100, - from_frac: frac_100 * 10.0, - list_from_num: [num_100, num_100, num_100], - }, -} - -x_101 = 3.14 -y_101 = 1.23e45 -z_101 = 0.5 - -my_str_101 : Str -my_str_101 = "one" - -binops_101 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_101 : U64 -> U64 -add_one_101 = |n| n + 1 - -map_add_one_101 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_101 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_101 = |arg_one, arg_two| arg_one * arg_two - -num_101 = 42 -frac_101 = 4.2 -str_101 = "hello" - -# Polymorphic empty collections -empty_list_101 = [] - -# Mixed polymorphic structures -mixed_101 = { - numbers: { value: num_101, list: [num_101, num_101], float: frac }, - strings: { value: str_101, list: [str_101, str_101] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_101 }, - }, - computations: { - from_num: num_101 * 100, - from_frac: frac_101 * 10.0, - list_from_num: [num_101, num_101, num_101], - }, -} - -x_102 = 3.14 -y_102 = 1.23e45 -z_102 = 0.5 - -my_str_102 : Str -my_str_102 = "one" - -binops_102 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_102 : U64 -> U64 -add_one_102 = |n| n + 1 - -map_add_one_102 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_102 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_102 = |arg_one, arg_two| arg_one * arg_two - -num_102 = 42 -frac_102 = 4.2 -str_102 = "hello" - -# Polymorphic empty collections -empty_list_102 = [] - -# Mixed polymorphic structures -mixed_102 = { - numbers: { value: num_102, list: [num_102, num_102], float: frac }, - strings: { value: str_102, list: [str_102, str_102] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_102 }, - }, - computations: { - from_num: num_102 * 100, - from_frac: frac_102 * 10.0, - list_from_num: [num_102, num_102, num_102], - }, -} - -x_103 = 3.14 -y_103 = 1.23e45 -z_103 = 0.5 - -my_str_103 : Str -my_str_103 = "one" - -binops_103 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_103 : U64 -> U64 -add_one_103 = |n| n + 1 - -map_add_one_103 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_103 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_103 = |arg_one, arg_two| arg_one * arg_two - -num_103 = 42 -frac_103 = 4.2 -str_103 = "hello" - -# Polymorphic empty collections -empty_list_103 = [] - -# Mixed polymorphic structures -mixed_103 = { - numbers: { value: num_103, list: [num_103, num_103], float: frac }, - strings: { value: str_103, list: [str_103, str_103] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_103 }, - }, - computations: { - from_num: num_103 * 100, - from_frac: frac_103 * 10.0, - list_from_num: [num_103, num_103, num_103], - }, -} - -x_104 = 3.14 -y_104 = 1.23e45 -z_104 = 0.5 - -my_str_104 : Str -my_str_104 = "one" - -binops_104 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_104 : U64 -> U64 -add_one_104 = |n| n + 1 - -map_add_one_104 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_104 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_104 = |arg_one, arg_two| arg_one * arg_two - -num_104 = 42 -frac_104 = 4.2 -str_104 = "hello" - -# Polymorphic empty collections -empty_list_104 = [] - -# Mixed polymorphic structures -mixed_104 = { - numbers: { value: num_104, list: [num_104, num_104], float: frac }, - strings: { value: str_104, list: [str_104, str_104] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_104 }, - }, - computations: { - from_num: num_104 * 100, - from_frac: frac_104 * 10.0, - list_from_num: [num_104, num_104, num_104], - }, -} - -x_105 = 3.14 -y_105 = 1.23e45 -z_105 = 0.5 - -my_str_105 : Str -my_str_105 = "one" - -binops_105 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_105 : U64 -> U64 -add_one_105 = |n| n + 1 - -map_add_one_105 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_105 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_105 = |arg_one, arg_two| arg_one * arg_two - -num_105 = 42 -frac_105 = 4.2 -str_105 = "hello" - -# Polymorphic empty collections -empty_list_105 = [] - -# Mixed polymorphic structures -mixed_105 = { - numbers: { value: num_105, list: [num_105, num_105], float: frac }, - strings: { value: str_105, list: [str_105, str_105] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_105 }, - }, - computations: { - from_num: num_105 * 100, - from_frac: frac_105 * 10.0, - list_from_num: [num_105, num_105, num_105], - }, -} - -x_106 = 3.14 -y_106 = 1.23e45 -z_106 = 0.5 - -my_str_106 : Str -my_str_106 = "one" - -binops_106 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_106 : U64 -> U64 -add_one_106 = |n| n + 1 - -map_add_one_106 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_106 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_106 = |arg_one, arg_two| arg_one * arg_two - -num_106 = 42 -frac_106 = 4.2 -str_106 = "hello" - -# Polymorphic empty collections -empty_list_106 = [] - -# Mixed polymorphic structures -mixed_106 = { - numbers: { value: num_106, list: [num_106, num_106], float: frac }, - strings: { value: str_106, list: [str_106, str_106] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_106 }, - }, - computations: { - from_num: num_106 * 100, - from_frac: frac_106 * 10.0, - list_from_num: [num_106, num_106, num_106], - }, -} - -x_107 = 3.14 -y_107 = 1.23e45 -z_107 = 0.5 - -my_str_107 : Str -my_str_107 = "one" - -binops_107 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_107 : U64 -> U64 -add_one_107 = |n| n + 1 - -map_add_one_107 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_107 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_107 = |arg_one, arg_two| arg_one * arg_two - -num_107 = 42 -frac_107 = 4.2 -str_107 = "hello" - -# Polymorphic empty collections -empty_list_107 = [] - -# Mixed polymorphic structures -mixed_107 = { - numbers: { value: num_107, list: [num_107, num_107], float: frac }, - strings: { value: str_107, list: [str_107, str_107] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_107 }, - }, - computations: { - from_num: num_107 * 100, - from_frac: frac_107 * 10.0, - list_from_num: [num_107, num_107, num_107], - }, -} - -x_108 = 3.14 -y_108 = 1.23e45 -z_108 = 0.5 - -my_str_108 : Str -my_str_108 = "one" - -binops_108 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_108 : U64 -> U64 -add_one_108 = |n| n + 1 - -map_add_one_108 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_108 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_108 = |arg_one, arg_two| arg_one * arg_two - -num_108 = 42 -frac_108 = 4.2 -str_108 = "hello" - -# Polymorphic empty collections -empty_list_108 = [] - -# Mixed polymorphic structures -mixed_108 = { - numbers: { value: num_108, list: [num_108, num_108], float: frac }, - strings: { value: str_108, list: [str_108, str_108] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_108 }, - }, - computations: { - from_num: num_108 * 100, - from_frac: frac_108 * 10.0, - list_from_num: [num_108, num_108, num_108], - }, -} - -x_109 = 3.14 -y_109 = 1.23e45 -z_109 = 0.5 - -my_str_109 : Str -my_str_109 = "one" - -binops_109 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_109 : U64 -> U64 -add_one_109 = |n| n + 1 - -map_add_one_109 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_109 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_109 = |arg_one, arg_two| arg_one * arg_two - -num_109 = 42 -frac_109 = 4.2 -str_109 = "hello" - -# Polymorphic empty collections -empty_list_109 = [] - -# Mixed polymorphic structures -mixed_109 = { - numbers: { value: num_109, list: [num_109, num_109], float: frac }, - strings: { value: str_109, list: [str_109, str_109] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_109 }, - }, - computations: { - from_num: num_109 * 100, - from_frac: frac_109 * 10.0, - list_from_num: [num_109, num_109, num_109], - }, -} - -x_110 = 3.14 -y_110 = 1.23e45 -z_110 = 0.5 - -my_str_110 : Str -my_str_110 = "one" - -binops_110 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_110 : U64 -> U64 -add_one_110 = |n| n + 1 - -map_add_one_110 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_110 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_110 = |arg_one, arg_two| arg_one * arg_two - -num_110 = 42 -frac_110 = 4.2 -str_110 = "hello" - -# Polymorphic empty collections -empty_list_110 = [] - -# Mixed polymorphic structures -mixed_110 = { - numbers: { value: num_110, list: [num_110, num_110], float: frac }, - strings: { value: str_110, list: [str_110, str_110] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_110 }, - }, - computations: { - from_num: num_110 * 100, - from_frac: frac_110 * 10.0, - list_from_num: [num_110, num_110, num_110], - }, -} - -x_111 = 3.14 -y_111 = 1.23e45 -z_111 = 0.5 - -my_str_111 : Str -my_str_111 = "one" - -binops_111 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_111 : U64 -> U64 -add_one_111 = |n| n + 1 - -map_add_one_111 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_111 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_111 = |arg_one, arg_two| arg_one * arg_two - -num_111 = 42 -frac_111 = 4.2 -str_111 = "hello" - -# Polymorphic empty collections -empty_list_111 = [] - -# Mixed polymorphic structures -mixed_111 = { - numbers: { value: num_111, list: [num_111, num_111], float: frac }, - strings: { value: str_111, list: [str_111, str_111] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_111 }, - }, - computations: { - from_num: num_111 * 100, - from_frac: frac_111 * 10.0, - list_from_num: [num_111, num_111, num_111], - }, -} - -x_112 = 3.14 -y_112 = 1.23e45 -z_112 = 0.5 - -my_str_112 : Str -my_str_112 = "one" - -binops_112 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_112 : U64 -> U64 -add_one_112 = |n| n + 1 - -map_add_one_112 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_112 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_112 = |arg_one, arg_two| arg_one * arg_two - -num_112 = 42 -frac_112 = 4.2 -str_112 = "hello" - -# Polymorphic empty collections -empty_list_112 = [] - -# Mixed polymorphic structures -mixed_112 = { - numbers: { value: num_112, list: [num_112, num_112], float: frac }, - strings: { value: str_112, list: [str_112, str_112] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_112 }, - }, - computations: { - from_num: num_112 * 100, - from_frac: frac_112 * 10.0, - list_from_num: [num_112, num_112, num_112], - }, -} - -x_113 = 3.14 -y_113 = 1.23e45 -z_113 = 0.5 - -my_str_113 : Str -my_str_113 = "one" - -binops_113 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_113 : U64 -> U64 -add_one_113 = |n| n + 1 - -map_add_one_113 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_113 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_113 = |arg_one, arg_two| arg_one * arg_two - -num_113 = 42 -frac_113 = 4.2 -str_113 = "hello" - -# Polymorphic empty collections -empty_list_113 = [] - -# Mixed polymorphic structures -mixed_113 = { - numbers: { value: num_113, list: [num_113, num_113], float: frac }, - strings: { value: str_113, list: [str_113, str_113] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_113 }, - }, - computations: { - from_num: num_113 * 100, - from_frac: frac_113 * 10.0, - list_from_num: [num_113, num_113, num_113], - }, -} - -x_114 = 3.14 -y_114 = 1.23e45 -z_114 = 0.5 - -my_str_114 : Str -my_str_114 = "one" - -binops_114 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_114 : U64 -> U64 -add_one_114 = |n| n + 1 - -map_add_one_114 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_114 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_114 = |arg_one, arg_two| arg_one * arg_two - -num_114 = 42 -frac_114 = 4.2 -str_114 = "hello" - -# Polymorphic empty collections -empty_list_114 = [] - -# Mixed polymorphic structures -mixed_114 = { - numbers: { value: num_114, list: [num_114, num_114], float: frac }, - strings: { value: str_114, list: [str_114, str_114] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_114 }, - }, - computations: { - from_num: num_114 * 100, - from_frac: frac_114 * 10.0, - list_from_num: [num_114, num_114, num_114], - }, -} - -x_115 = 3.14 -y_115 = 1.23e45 -z_115 = 0.5 - -my_str_115 : Str -my_str_115 = "one" - -binops_115 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_115 : U64 -> U64 -add_one_115 = |n| n + 1 - -map_add_one_115 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_115 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_115 = |arg_one, arg_two| arg_one * arg_two - -num_115 = 42 -frac_115 = 4.2 -str_115 = "hello" - -# Polymorphic empty collections -empty_list_115 = [] - -# Mixed polymorphic structures -mixed_115 = { - numbers: { value: num_115, list: [num_115, num_115], float: frac }, - strings: { value: str_115, list: [str_115, str_115] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_115 }, - }, - computations: { - from_num: num_115 * 100, - from_frac: frac_115 * 10.0, - list_from_num: [num_115, num_115, num_115], - }, -} - -x_116 = 3.14 -y_116 = 1.23e45 -z_116 = 0.5 - -my_str_116 : Str -my_str_116 = "one" - -binops_116 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_116 : U64 -> U64 -add_one_116 = |n| n + 1 - -map_add_one_116 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_116 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_116 = |arg_one, arg_two| arg_one * arg_two - -num_116 = 42 -frac_116 = 4.2 -str_116 = "hello" - -# Polymorphic empty collections -empty_list_116 = [] - -# Mixed polymorphic structures -mixed_116 = { - numbers: { value: num_116, list: [num_116, num_116], float: frac }, - strings: { value: str_116, list: [str_116, str_116] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_116 }, - }, - computations: { - from_num: num_116 * 100, - from_frac: frac_116 * 10.0, - list_from_num: [num_116, num_116, num_116], - }, -} - -x_117 = 3.14 -y_117 = 1.23e45 -z_117 = 0.5 - -my_str_117 : Str -my_str_117 = "one" - -binops_117 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_117 : U64 -> U64 -add_one_117 = |n| n + 1 - -map_add_one_117 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_117 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_117 = |arg_one, arg_two| arg_one * arg_two - -num_117 = 42 -frac_117 = 4.2 -str_117 = "hello" - -# Polymorphic empty collections -empty_list_117 = [] - -# Mixed polymorphic structures -mixed_117 = { - numbers: { value: num_117, list: [num_117, num_117], float: frac }, - strings: { value: str_117, list: [str_117, str_117] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_117 }, - }, - computations: { - from_num: num_117 * 100, - from_frac: frac_117 * 10.0, - list_from_num: [num_117, num_117, num_117], - }, -} - -x_118 = 3.14 -y_118 = 1.23e45 -z_118 = 0.5 - -my_str_118 : Str -my_str_118 = "one" - -binops_118 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_118 : U64 -> U64 -add_one_118 = |n| n + 1 - -map_add_one_118 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_118 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_118 = |arg_one, arg_two| arg_one * arg_two - -num_118 = 42 -frac_118 = 4.2 -str_118 = "hello" - -# Polymorphic empty collections -empty_list_118 = [] - -# Mixed polymorphic structures -mixed_118 = { - numbers: { value: num_118, list: [num_118, num_118], float: frac }, - strings: { value: str_118, list: [str_118, str_118] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_118 }, - }, - computations: { - from_num: num_118 * 100, - from_frac: frac_118 * 10.0, - list_from_num: [num_118, num_118, num_118], - }, -} - -x_119 = 3.14 -y_119 = 1.23e45 -z_119 = 0.5 - -my_str_119 : Str -my_str_119 = "one" - -binops_119 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_119 : U64 -> U64 -add_one_119 = |n| n + 1 - -map_add_one_119 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_119 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_119 = |arg_one, arg_two| arg_one * arg_two - -num_119 = 42 -frac_119 = 4.2 -str_119 = "hello" - -# Polymorphic empty collections -empty_list_119 = [] - -# Mixed polymorphic structures -mixed_119 = { - numbers: { value: num_119, list: [num_119, num_119], float: frac }, - strings: { value: str_119, list: [str_119, str_119] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_119 }, - }, - computations: { - from_num: num_119 * 100, - from_frac: frac_119 * 10.0, - list_from_num: [num_119, num_119, num_119], - }, -} - -x_120 = 3.14 -y_120 = 1.23e45 -z_120 = 0.5 - -my_str_120 : Str -my_str_120 = "one" - -binops_120 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_120 : U64 -> U64 -add_one_120 = |n| n + 1 - -map_add_one_120 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_120 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_120 = |arg_one, arg_two| arg_one * arg_two - -num_120 = 42 -frac_120 = 4.2 -str_120 = "hello" - -# Polymorphic empty collections -empty_list_120 = [] - -# Mixed polymorphic structures -mixed_120 = { - numbers: { value: num_120, list: [num_120, num_120], float: frac }, - strings: { value: str_120, list: [str_120, str_120] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_120 }, - }, - computations: { - from_num: num_120 * 100, - from_frac: frac_120 * 10.0, - list_from_num: [num_120, num_120, num_120], - }, -} - -x_121 = 3.14 -y_121 = 1.23e45 -z_121 = 0.5 - -my_str_121 : Str -my_str_121 = "one" - -binops_121 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_121 : U64 -> U64 -add_one_121 = |n| n + 1 - -map_add_one_121 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_121 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_121 = |arg_one, arg_two| arg_one * arg_two - -num_121 = 42 -frac_121 = 4.2 -str_121 = "hello" - -# Polymorphic empty collections -empty_list_121 = [] - -# Mixed polymorphic structures -mixed_121 = { - numbers: { value: num_121, list: [num_121, num_121], float: frac }, - strings: { value: str_121, list: [str_121, str_121] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_121 }, - }, - computations: { - from_num: num_121 * 100, - from_frac: frac_121 * 10.0, - list_from_num: [num_121, num_121, num_121], - }, -} - -x_122 = 3.14 -y_122 = 1.23e45 -z_122 = 0.5 - -my_str_122 : Str -my_str_122 = "one" - -binops_122 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_122 : U64 -> U64 -add_one_122 = |n| n + 1 - -map_add_one_122 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_122 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_122 = |arg_one, arg_two| arg_one * arg_two - -num_122 = 42 -frac_122 = 4.2 -str_122 = "hello" - -# Polymorphic empty collections -empty_list_122 = [] - -# Mixed polymorphic structures -mixed_122 = { - numbers: { value: num_122, list: [num_122, num_122], float: frac }, - strings: { value: str_122, list: [str_122, str_122] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_122 }, - }, - computations: { - from_num: num_122 * 100, - from_frac: frac_122 * 10.0, - list_from_num: [num_122, num_122, num_122], - }, -} - -x_123 = 3.14 -y_123 = 1.23e45 -z_123 = 0.5 - -my_str_123 : Str -my_str_123 = "one" - -binops_123 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_123 : U64 -> U64 -add_one_123 = |n| n + 1 - -map_add_one_123 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_123 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_123 = |arg_one, arg_two| arg_one * arg_two - -num_123 = 42 -frac_123 = 4.2 -str_123 = "hello" - -# Polymorphic empty collections -empty_list_123 = [] - -# Mixed polymorphic structures -mixed_123 = { - numbers: { value: num_123, list: [num_123, num_123], float: frac }, - strings: { value: str_123, list: [str_123, str_123] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_123 }, - }, - computations: { - from_num: num_123 * 100, - from_frac: frac_123 * 10.0, - list_from_num: [num_123, num_123, num_123], - }, -} - -x_124 = 3.14 -y_124 = 1.23e45 -z_124 = 0.5 - -my_str_124 : Str -my_str_124 = "one" - -binops_124 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_124 : U64 -> U64 -add_one_124 = |n| n + 1 - -map_add_one_124 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_124 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_124 = |arg_one, arg_two| arg_one * arg_two - -num_124 = 42 -frac_124 = 4.2 -str_124 = "hello" - -# Polymorphic empty collections -empty_list_124 = [] - -# Mixed polymorphic structures -mixed_124 = { - numbers: { value: num_124, list: [num_124, num_124], float: frac }, - strings: { value: str_124, list: [str_124, str_124] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_124 }, - }, - computations: { - from_num: num_124 * 100, - from_frac: frac_124 * 10.0, - list_from_num: [num_124, num_124, num_124], - }, -} - -x_125 = 3.14 -y_125 = 1.23e45 -z_125 = 0.5 - -my_str_125 : Str -my_str_125 = "one" - -binops_125 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_125 : U64 -> U64 -add_one_125 = |n| n + 1 - -map_add_one_125 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_125 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_125 = |arg_one, arg_two| arg_one * arg_two - -num_125 = 42 -frac_125 = 4.2 -str_125 = "hello" - -# Polymorphic empty collections -empty_list_125 = [] - -# Mixed polymorphic structures -mixed_125 = { - numbers: { value: num_125, list: [num_125, num_125], float: frac }, - strings: { value: str_125, list: [str_125, str_125] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_125 }, - }, - computations: { - from_num: num_125 * 100, - from_frac: frac_125 * 10.0, - list_from_num: [num_125, num_125, num_125], - }, -} - -x_126 = 3.14 -y_126 = 1.23e45 -z_126 = 0.5 - -my_str_126 : Str -my_str_126 = "one" - -binops_126 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_126 : U64 -> U64 -add_one_126 = |n| n + 1 - -map_add_one_126 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_126 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_126 = |arg_one, arg_two| arg_one * arg_two - -num_126 = 42 -frac_126 = 4.2 -str_126 = "hello" - -# Polymorphic empty collections -empty_list_126 = [] - -# Mixed polymorphic structures -mixed_126 = { - numbers: { value: num_126, list: [num_126, num_126], float: frac }, - strings: { value: str_126, list: [str_126, str_126] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_126 }, - }, - computations: { - from_num: num_126 * 100, - from_frac: frac_126 * 10.0, - list_from_num: [num_126, num_126, num_126], - }, -} - -x_127 = 3.14 -y_127 = 1.23e45 -z_127 = 0.5 - -my_str_127 : Str -my_str_127 = "one" - -binops_127 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_127 : U64 -> U64 -add_one_127 = |n| n + 1 - -map_add_one_127 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_127 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_127 = |arg_one, arg_two| arg_one * arg_two - -num_127 = 42 -frac_127 = 4.2 -str_127 = "hello" - -# Polymorphic empty collections -empty_list_127 = [] - -# Mixed polymorphic structures -mixed_127 = { - numbers: { value: num_127, list: [num_127, num_127], float: frac }, - strings: { value: str_127, list: [str_127, str_127] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_127 }, - }, - computations: { - from_num: num_127 * 100, - from_frac: frac_127 * 10.0, - list_from_num: [num_127, num_127, num_127], - }, -} - -x_128 = 3.14 -y_128 = 1.23e45 -z_128 = 0.5 - -my_str_128 : Str -my_str_128 = "one" - -binops_128 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_128 : U64 -> U64 -add_one_128 = |n| n + 1 - -map_add_one_128 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_128 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_128 = |arg_one, arg_two| arg_one * arg_two - -num_128 = 42 -frac_128 = 4.2 -str_128 = "hello" - -# Polymorphic empty collections -empty_list_128 = [] - -# Mixed polymorphic structures -mixed_128 = { - numbers: { value: num_128, list: [num_128, num_128], float: frac }, - strings: { value: str_128, list: [str_128, str_128] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_128 }, - }, - computations: { - from_num: num_128 * 100, - from_frac: frac_128 * 10.0, - list_from_num: [num_128, num_128, num_128], - }, -} - -x_129 = 3.14 -y_129 = 1.23e45 -z_129 = 0.5 - -my_str_129 : Str -my_str_129 = "one" - -binops_129 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_129 : U64 -> U64 -add_one_129 = |n| n + 1 - -map_add_one_129 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_129 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_129 = |arg_one, arg_two| arg_one * arg_two - -num_129 = 42 -frac_129 = 4.2 -str_129 = "hello" - -# Polymorphic empty collections -empty_list_129 = [] - -# Mixed polymorphic structures -mixed_129 = { - numbers: { value: num_129, list: [num_129, num_129], float: frac }, - strings: { value: str_129, list: [str_129, str_129] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_129 }, - }, - computations: { - from_num: num_129 * 100, - from_frac: frac_129 * 10.0, - list_from_num: [num_129, num_129, num_129], - }, -} - -x_130 = 3.14 -y_130 = 1.23e45 -z_130 = 0.5 - -my_str_130 : Str -my_str_130 = "one" - -binops_130 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_130 : U64 -> U64 -add_one_130 = |n| n + 1 - -map_add_one_130 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_130 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_130 = |arg_one, arg_two| arg_one * arg_two - -num_130 = 42 -frac_130 = 4.2 -str_130 = "hello" - -# Polymorphic empty collections -empty_list_130 = [] - -# Mixed polymorphic structures -mixed_130 = { - numbers: { value: num_130, list: [num_130, num_130], float: frac }, - strings: { value: str_130, list: [str_130, str_130] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_130 }, - }, - computations: { - from_num: num_130 * 100, - from_frac: frac_130 * 10.0, - list_from_num: [num_130, num_130, num_130], - }, -} - -x_131 = 3.14 -y_131 = 1.23e45 -z_131 = 0.5 - -my_str_131 : Str -my_str_131 = "one" - -binops_131 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_131 : U64 -> U64 -add_one_131 = |n| n + 1 - -map_add_one_131 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_131 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_131 = |arg_one, arg_two| arg_one * arg_two - -num_131 = 42 -frac_131 = 4.2 -str_131 = "hello" - -# Polymorphic empty collections -empty_list_131 = [] - -# Mixed polymorphic structures -mixed_131 = { - numbers: { value: num_131, list: [num_131, num_131], float: frac }, - strings: { value: str_131, list: [str_131, str_131] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_131 }, - }, - computations: { - from_num: num_131 * 100, - from_frac: frac_131 * 10.0, - list_from_num: [num_131, num_131, num_131], - }, -} - -x_132 = 3.14 -y_132 = 1.23e45 -z_132 = 0.5 - -my_str_132 : Str -my_str_132 = "one" - -binops_132 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_132 : U64 -> U64 -add_one_132 = |n| n + 1 - -map_add_one_132 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_132 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_132 = |arg_one, arg_two| arg_one * arg_two - -num_132 = 42 -frac_132 = 4.2 -str_132 = "hello" - -# Polymorphic empty collections -empty_list_132 = [] - -# Mixed polymorphic structures -mixed_132 = { - numbers: { value: num_132, list: [num_132, num_132], float: frac }, - strings: { value: str_132, list: [str_132, str_132] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_132 }, - }, - computations: { - from_num: num_132 * 100, - from_frac: frac_132 * 10.0, - list_from_num: [num_132, num_132, num_132], - }, -} - -x_133 = 3.14 -y_133 = 1.23e45 -z_133 = 0.5 - -my_str_133 : Str -my_str_133 = "one" - -binops_133 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_133 : U64 -> U64 -add_one_133 = |n| n + 1 - -map_add_one_133 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_133 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_133 = |arg_one, arg_two| arg_one * arg_two - -num_133 = 42 -frac_133 = 4.2 -str_133 = "hello" - -# Polymorphic empty collections -empty_list_133 = [] - -# Mixed polymorphic structures -mixed_133 = { - numbers: { value: num_133, list: [num_133, num_133], float: frac }, - strings: { value: str_133, list: [str_133, str_133] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_133 }, - }, - computations: { - from_num: num_133 * 100, - from_frac: frac_133 * 10.0, - list_from_num: [num_133, num_133, num_133], - }, -} - -x_134 = 3.14 -y_134 = 1.23e45 -z_134 = 0.5 - -my_str_134 : Str -my_str_134 = "one" - -binops_134 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_134 : U64 -> U64 -add_one_134 = |n| n + 1 - -map_add_one_134 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_134 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_134 = |arg_one, arg_two| arg_one * arg_two - -num_134 = 42 -frac_134 = 4.2 -str_134 = "hello" - -# Polymorphic empty collections -empty_list_134 = [] - -# Mixed polymorphic structures -mixed_134 = { - numbers: { value: num_134, list: [num_134, num_134], float: frac }, - strings: { value: str_134, list: [str_134, str_134] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_134 }, - }, - computations: { - from_num: num_134 * 100, - from_frac: frac_134 * 10.0, - list_from_num: [num_134, num_134, num_134], - }, -} - -x_135 = 3.14 -y_135 = 1.23e45 -z_135 = 0.5 - -my_str_135 : Str -my_str_135 = "one" - -binops_135 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_135 : U64 -> U64 -add_one_135 = |n| n + 1 - -map_add_one_135 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_135 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_135 = |arg_one, arg_two| arg_one * arg_two - -num_135 = 42 -frac_135 = 4.2 -str_135 = "hello" - -# Polymorphic empty collections -empty_list_135 = [] - -# Mixed polymorphic structures -mixed_135 = { - numbers: { value: num_135, list: [num_135, num_135], float: frac }, - strings: { value: str_135, list: [str_135, str_135] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_135 }, - }, - computations: { - from_num: num_135 * 100, - from_frac: frac_135 * 10.0, - list_from_num: [num_135, num_135, num_135], - }, -} - -x_136 = 3.14 -y_136 = 1.23e45 -z_136 = 0.5 - -my_str_136 : Str -my_str_136 = "one" - -binops_136 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_136 : U64 -> U64 -add_one_136 = |n| n + 1 - -map_add_one_136 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_136 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_136 = |arg_one, arg_two| arg_one * arg_two - -num_136 = 42 -frac_136 = 4.2 -str_136 = "hello" - -# Polymorphic empty collections -empty_list_136 = [] - -# Mixed polymorphic structures -mixed_136 = { - numbers: { value: num_136, list: [num_136, num_136], float: frac }, - strings: { value: str_136, list: [str_136, str_136] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_136 }, - }, - computations: { - from_num: num_136 * 100, - from_frac: frac_136 * 10.0, - list_from_num: [num_136, num_136, num_136], - }, -} - -x_137 = 3.14 -y_137 = 1.23e45 -z_137 = 0.5 - -my_str_137 : Str -my_str_137 = "one" - -binops_137 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_137 : U64 -> U64 -add_one_137 = |n| n + 1 - -map_add_one_137 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_137 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_137 = |arg_one, arg_two| arg_one * arg_two - -num_137 = 42 -frac_137 = 4.2 -str_137 = "hello" - -# Polymorphic empty collections -empty_list_137 = [] - -# Mixed polymorphic structures -mixed_137 = { - numbers: { value: num_137, list: [num_137, num_137], float: frac }, - strings: { value: str_137, list: [str_137, str_137] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_137 }, - }, - computations: { - from_num: num_137 * 100, - from_frac: frac_137 * 10.0, - list_from_num: [num_137, num_137, num_137], - }, -} - -x_138 = 3.14 -y_138 = 1.23e45 -z_138 = 0.5 - -my_str_138 : Str -my_str_138 = "one" - -binops_138 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_138 : U64 -> U64 -add_one_138 = |n| n + 1 - -map_add_one_138 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_138 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_138 = |arg_one, arg_two| arg_one * arg_two - -num_138 = 42 -frac_138 = 4.2 -str_138 = "hello" - -# Polymorphic empty collections -empty_list_138 = [] - -# Mixed polymorphic structures -mixed_138 = { - numbers: { value: num_138, list: [num_138, num_138], float: frac }, - strings: { value: str_138, list: [str_138, str_138] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_138 }, - }, - computations: { - from_num: num_138 * 100, - from_frac: frac_138 * 10.0, - list_from_num: [num_138, num_138, num_138], - }, -} - -x_139 = 3.14 -y_139 = 1.23e45 -z_139 = 0.5 - -my_str_139 : Str -my_str_139 = "one" - -binops_139 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_139 : U64 -> U64 -add_one_139 = |n| n + 1 - -map_add_one_139 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_139 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_139 = |arg_one, arg_two| arg_one * arg_two - -num_139 = 42 -frac_139 = 4.2 -str_139 = "hello" - -# Polymorphic empty collections -empty_list_139 = [] - -# Mixed polymorphic structures -mixed_139 = { - numbers: { value: num_139, list: [num_139, num_139], float: frac }, - strings: { value: str_139, list: [str_139, str_139] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_139 }, - }, - computations: { - from_num: num_139 * 100, - from_frac: frac_139 * 10.0, - list_from_num: [num_139, num_139, num_139], - }, -} - -x_140 = 3.14 -y_140 = 1.23e45 -z_140 = 0.5 - -my_str_140 : Str -my_str_140 = "one" - -binops_140 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_140 : U64 -> U64 -add_one_140 = |n| n + 1 - -map_add_one_140 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_140 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_140 = |arg_one, arg_two| arg_one * arg_two - -num_140 = 42 -frac_140 = 4.2 -str_140 = "hello" - -# Polymorphic empty collections -empty_list_140 = [] - -# Mixed polymorphic structures -mixed_140 = { - numbers: { value: num_140, list: [num_140, num_140], float: frac }, - strings: { value: str_140, list: [str_140, str_140] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_140 }, - }, - computations: { - from_num: num_140 * 100, - from_frac: frac_140 * 10.0, - list_from_num: [num_140, num_140, num_140], - }, -} - -x_141 = 3.14 -y_141 = 1.23e45 -z_141 = 0.5 - -my_str_141 : Str -my_str_141 = "one" - -binops_141 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_141 : U64 -> U64 -add_one_141 = |n| n + 1 - -map_add_one_141 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_141 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_141 = |arg_one, arg_two| arg_one * arg_two - -num_141 = 42 -frac_141 = 4.2 -str_141 = "hello" - -# Polymorphic empty collections -empty_list_141 = [] - -# Mixed polymorphic structures -mixed_141 = { - numbers: { value: num_141, list: [num_141, num_141], float: frac }, - strings: { value: str_141, list: [str_141, str_141] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_141 }, - }, - computations: { - from_num: num_141 * 100, - from_frac: frac_141 * 10.0, - list_from_num: [num_141, num_141, num_141], - }, -} - -x_142 = 3.14 -y_142 = 1.23e45 -z_142 = 0.5 - -my_str_142 : Str -my_str_142 = "one" - -binops_142 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_142 : U64 -> U64 -add_one_142 = |n| n + 1 - -map_add_one_142 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_142 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_142 = |arg_one, arg_two| arg_one * arg_two - -num_142 = 42 -frac_142 = 4.2 -str_142 = "hello" - -# Polymorphic empty collections -empty_list_142 = [] - -# Mixed polymorphic structures -mixed_142 = { - numbers: { value: num_142, list: [num_142, num_142], float: frac }, - strings: { value: str_142, list: [str_142, str_142] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_142 }, - }, - computations: { - from_num: num_142 * 100, - from_frac: frac_142 * 10.0, - list_from_num: [num_142, num_142, num_142], - }, -} - -x_143 = 3.14 -y_143 = 1.23e45 -z_143 = 0.5 - -my_str_143 : Str -my_str_143 = "one" - -binops_143 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_143 : U64 -> U64 -add_one_143 = |n| n + 1 - -map_add_one_143 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_143 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_143 = |arg_one, arg_two| arg_one * arg_two - -num_143 = 42 -frac_143 = 4.2 -str_143 = "hello" - -# Polymorphic empty collections -empty_list_143 = [] - -# Mixed polymorphic structures -mixed_143 = { - numbers: { value: num_143, list: [num_143, num_143], float: frac }, - strings: { value: str_143, list: [str_143, str_143] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_143 }, - }, - computations: { - from_num: num_143 * 100, - from_frac: frac_143 * 10.0, - list_from_num: [num_143, num_143, num_143], - }, -} - -x_144 = 3.14 -y_144 = 1.23e45 -z_144 = 0.5 - -my_str_144 : Str -my_str_144 = "one" - -binops_144 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_144 : U64 -> U64 -add_one_144 = |n| n + 1 - -map_add_one_144 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_144 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_144 = |arg_one, arg_two| arg_one * arg_two - -num_144 = 42 -frac_144 = 4.2 -str_144 = "hello" - -# Polymorphic empty collections -empty_list_144 = [] - -# Mixed polymorphic structures -mixed_144 = { - numbers: { value: num_144, list: [num_144, num_144], float: frac }, - strings: { value: str_144, list: [str_144, str_144] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_144 }, - }, - computations: { - from_num: num_144 * 100, - from_frac: frac_144 * 10.0, - list_from_num: [num_144, num_144, num_144], - }, -} - -x_145 = 3.14 -y_145 = 1.23e45 -z_145 = 0.5 - -my_str_145 : Str -my_str_145 = "one" - -binops_145 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_145 : U64 -> U64 -add_one_145 = |n| n + 1 - -map_add_one_145 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_145 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_145 = |arg_one, arg_two| arg_one * arg_two - -num_145 = 42 -frac_145 = 4.2 -str_145 = "hello" - -# Polymorphic empty collections -empty_list_145 = [] - -# Mixed polymorphic structures -mixed_145 = { - numbers: { value: num_145, list: [num_145, num_145], float: frac }, - strings: { value: str_145, list: [str_145, str_145] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_145 }, - }, - computations: { - from_num: num_145 * 100, - from_frac: frac_145 * 10.0, - list_from_num: [num_145, num_145, num_145], - }, -} - -x_146 = 3.14 -y_146 = 1.23e45 -z_146 = 0.5 - -my_str_146 : Str -my_str_146 = "one" - -binops_146 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_146 : U64 -> U64 -add_one_146 = |n| n + 1 - -map_add_one_146 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_146 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_146 = |arg_one, arg_two| arg_one * arg_two - -num_146 = 42 -frac_146 = 4.2 -str_146 = "hello" - -# Polymorphic empty collections -empty_list_146 = [] - -# Mixed polymorphic structures -mixed_146 = { - numbers: { value: num_146, list: [num_146, num_146], float: frac }, - strings: { value: str_146, list: [str_146, str_146] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_146 }, - }, - computations: { - from_num: num_146 * 100, - from_frac: frac_146 * 10.0, - list_from_num: [num_146, num_146, num_146], - }, -} - -x_147 = 3.14 -y_147 = 1.23e45 -z_147 = 0.5 - -my_str_147 : Str -my_str_147 = "one" - -binops_147 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_147 : U64 -> U64 -add_one_147 = |n| n + 1 - -map_add_one_147 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_147 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_147 = |arg_one, arg_two| arg_one * arg_two - -num_147 = 42 -frac_147 = 4.2 -str_147 = "hello" - -# Polymorphic empty collections -empty_list_147 = [] - -# Mixed polymorphic structures -mixed_147 = { - numbers: { value: num_147, list: [num_147, num_147], float: frac }, - strings: { value: str_147, list: [str_147, str_147] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_147 }, - }, - computations: { - from_num: num_147 * 100, - from_frac: frac_147 * 10.0, - list_from_num: [num_147, num_147, num_147], - }, -} - -x_148 = 3.14 -y_148 = 1.23e45 -z_148 = 0.5 - -my_str_148 : Str -my_str_148 = "one" - -binops_148 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_148 : U64 -> U64 -add_one_148 = |n| n + 1 - -map_add_one_148 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_148 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_148 = |arg_one, arg_two| arg_one * arg_two - -num_148 = 42 -frac_148 = 4.2 -str_148 = "hello" - -# Polymorphic empty collections -empty_list_148 = [] - -# Mixed polymorphic structures -mixed_148 = { - numbers: { value: num_148, list: [num_148, num_148], float: frac }, - strings: { value: str_148, list: [str_148, str_148] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_148 }, - }, - computations: { - from_num: num_148 * 100, - from_frac: frac_148 * 10.0, - list_from_num: [num_148, num_148, num_148], - }, -} - -x_149 = 3.14 -y_149 = 1.23e45 -z_149 = 0.5 - -my_str_149 : Str -my_str_149 = "one" - -binops_149 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_149 : U64 -> U64 -add_one_149 = |n| n + 1 - -map_add_one_149 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_149 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_149 = |arg_one, arg_two| arg_one * arg_two - -num_149 = 42 -frac_149 = 4.2 -str_149 = "hello" - -# Polymorphic empty collections -empty_list_149 = [] - -# Mixed polymorphic structures -mixed_149 = { - numbers: { value: num_149, list: [num_149, num_149], float: frac }, - strings: { value: str_149, list: [str_149, str_149] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_149 }, - }, - computations: { - from_num: num_149 * 100, - from_frac: frac_149 * 10.0, - list_from_num: [num_149, num_149, num_149], - }, -} - -x_150 = 3.14 -y_150 = 1.23e45 -z_150 = 0.5 - -my_str_150 : Str -my_str_150 = "one" - -binops_150 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_150 : U64 -> U64 -add_one_150 = |n| n + 1 - -map_add_one_150 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_150 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_150 = |arg_one, arg_two| arg_one * arg_two - -num_150 = 42 -frac_150 = 4.2 -str_150 = "hello" - -# Polymorphic empty collections -empty_list_150 = [] - -# Mixed polymorphic structures -mixed_150 = { - numbers: { value: num_150, list: [num_150, num_150], float: frac }, - strings: { value: str_150, list: [str_150, str_150] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_150 }, - }, - computations: { - from_num: num_150 * 100, - from_frac: frac_150 * 10.0, - list_from_num: [num_150, num_150, num_150], - }, -} - -x_151 = 3.14 -y_151 = 1.23e45 -z_151 = 0.5 - -my_str_151 : Str -my_str_151 = "one" - -binops_151 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_151 : U64 -> U64 -add_one_151 = |n| n + 1 - -map_add_one_151 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_151 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_151 = |arg_one, arg_two| arg_one * arg_two - -num_151 = 42 -frac_151 = 4.2 -str_151 = "hello" - -# Polymorphic empty collections -empty_list_151 = [] - -# Mixed polymorphic structures -mixed_151 = { - numbers: { value: num_151, list: [num_151, num_151], float: frac }, - strings: { value: str_151, list: [str_151, str_151] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_151 }, - }, - computations: { - from_num: num_151 * 100, - from_frac: frac_151 * 10.0, - list_from_num: [num_151, num_151, num_151], - }, -} - -x_152 = 3.14 -y_152 = 1.23e45 -z_152 = 0.5 - -my_str_152 : Str -my_str_152 = "one" - -binops_152 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_152 : U64 -> U64 -add_one_152 = |n| n + 1 - -map_add_one_152 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_152 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_152 = |arg_one, arg_two| arg_one * arg_two - -num_152 = 42 -frac_152 = 4.2 -str_152 = "hello" - -# Polymorphic empty collections -empty_list_152 = [] - -# Mixed polymorphic structures -mixed_152 = { - numbers: { value: num_152, list: [num_152, num_152], float: frac }, - strings: { value: str_152, list: [str_152, str_152] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_152 }, - }, - computations: { - from_num: num_152 * 100, - from_frac: frac_152 * 10.0, - list_from_num: [num_152, num_152, num_152], - }, -} - -x_153 = 3.14 -y_153 = 1.23e45 -z_153 = 0.5 - -my_str_153 : Str -my_str_153 = "one" - -binops_153 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_153 : U64 -> U64 -add_one_153 = |n| n + 1 - -map_add_one_153 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_153 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_153 = |arg_one, arg_two| arg_one * arg_two - -num_153 = 42 -frac_153 = 4.2 -str_153 = "hello" - -# Polymorphic empty collections -empty_list_153 = [] - -# Mixed polymorphic structures -mixed_153 = { - numbers: { value: num_153, list: [num_153, num_153], float: frac }, - strings: { value: str_153, list: [str_153, str_153] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_153 }, - }, - computations: { - from_num: num_153 * 100, - from_frac: frac_153 * 10.0, - list_from_num: [num_153, num_153, num_153], - }, -} - -x_154 = 3.14 -y_154 = 1.23e45 -z_154 = 0.5 - -my_str_154 : Str -my_str_154 = "one" - -binops_154 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_154 : U64 -> U64 -add_one_154 = |n| n + 1 - -map_add_one_154 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_154 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_154 = |arg_one, arg_two| arg_one * arg_two - -num_154 = 42 -frac_154 = 4.2 -str_154 = "hello" - -# Polymorphic empty collections -empty_list_154 = [] - -# Mixed polymorphic structures -mixed_154 = { - numbers: { value: num_154, list: [num_154, num_154], float: frac }, - strings: { value: str_154, list: [str_154, str_154] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_154 }, - }, - computations: { - from_num: num_154 * 100, - from_frac: frac_154 * 10.0, - list_from_num: [num_154, num_154, num_154], - }, -} - -x_155 = 3.14 -y_155 = 1.23e45 -z_155 = 0.5 - -my_str_155 : Str -my_str_155 = "one" - -binops_155 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_155 : U64 -> U64 -add_one_155 = |n| n + 1 - -map_add_one_155 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_155 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_155 = |arg_one, arg_two| arg_one * arg_two - -num_155 = 42 -frac_155 = 4.2 -str_155 = "hello" - -# Polymorphic empty collections -empty_list_155 = [] - -# Mixed polymorphic structures -mixed_155 = { - numbers: { value: num_155, list: [num_155, num_155], float: frac }, - strings: { value: str_155, list: [str_155, str_155] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_155 }, - }, - computations: { - from_num: num_155 * 100, - from_frac: frac_155 * 10.0, - list_from_num: [num_155, num_155, num_155], - }, -} - -x_156 = 3.14 -y_156 = 1.23e45 -z_156 = 0.5 - -my_str_156 : Str -my_str_156 = "one" - -binops_156 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_156 : U64 -> U64 -add_one_156 = |n| n + 1 - -map_add_one_156 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_156 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_156 = |arg_one, arg_two| arg_one * arg_two - -num_156 = 42 -frac_156 = 4.2 -str_156 = "hello" - -# Polymorphic empty collections -empty_list_156 = [] - -# Mixed polymorphic structures -mixed_156 = { - numbers: { value: num_156, list: [num_156, num_156], float: frac }, - strings: { value: str_156, list: [str_156, str_156] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_156 }, - }, - computations: { - from_num: num_156 * 100, - from_frac: frac_156 * 10.0, - list_from_num: [num_156, num_156, num_156], - }, -} - -x_157 = 3.14 -y_157 = 1.23e45 -z_157 = 0.5 - -my_str_157 : Str -my_str_157 = "one" - -binops_157 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_157 : U64 -> U64 -add_one_157 = |n| n + 1 - -map_add_one_157 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_157 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_157 = |arg_one, arg_two| arg_one * arg_two - -num_157 = 42 -frac_157 = 4.2 -str_157 = "hello" - -# Polymorphic empty collections -empty_list_157 = [] - -# Mixed polymorphic structures -mixed_157 = { - numbers: { value: num_157, list: [num_157, num_157], float: frac }, - strings: { value: str_157, list: [str_157, str_157] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_157 }, - }, - computations: { - from_num: num_157 * 100, - from_frac: frac_157 * 10.0, - list_from_num: [num_157, num_157, num_157], - }, -} - -x_158 = 3.14 -y_158 = 1.23e45 -z_158 = 0.5 - -my_str_158 : Str -my_str_158 = "one" - -binops_158 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_158 : U64 -> U64 -add_one_158 = |n| n + 1 - -map_add_one_158 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_158 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_158 = |arg_one, arg_two| arg_one * arg_two - -num_158 = 42 -frac_158 = 4.2 -str_158 = "hello" - -# Polymorphic empty collections -empty_list_158 = [] - -# Mixed polymorphic structures -mixed_158 = { - numbers: { value: num_158, list: [num_158, num_158], float: frac }, - strings: { value: str_158, list: [str_158, str_158] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_158 }, - }, - computations: { - from_num: num_158 * 100, - from_frac: frac_158 * 10.0, - list_from_num: [num_158, num_158, num_158], - }, -} - -x_159 = 3.14 -y_159 = 1.23e45 -z_159 = 0.5 - -my_str_159 : Str -my_str_159 = "one" - -binops_159 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_159 : U64 -> U64 -add_one_159 = |n| n + 1 - -map_add_one_159 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_159 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_159 = |arg_one, arg_two| arg_one * arg_two - -num_159 = 42 -frac_159 = 4.2 -str_159 = "hello" - -# Polymorphic empty collections -empty_list_159 = [] - -# Mixed polymorphic structures -mixed_159 = { - numbers: { value: num_159, list: [num_159, num_159], float: frac }, - strings: { value: str_159, list: [str_159, str_159] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_159 }, - }, - computations: { - from_num: num_159 * 100, - from_frac: frac_159 * 10.0, - list_from_num: [num_159, num_159, num_159], - }, -} - -x_160 = 3.14 -y_160 = 1.23e45 -z_160 = 0.5 - -my_str_160 : Str -my_str_160 = "one" - -binops_160 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_160 : U64 -> U64 -add_one_160 = |n| n + 1 - -map_add_one_160 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_160 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_160 = |arg_one, arg_two| arg_one * arg_two - -num_160 = 42 -frac_160 = 4.2 -str_160 = "hello" - -# Polymorphic empty collections -empty_list_160 = [] - -# Mixed polymorphic structures -mixed_160 = { - numbers: { value: num_160, list: [num_160, num_160], float: frac }, - strings: { value: str_160, list: [str_160, str_160] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_160 }, - }, - computations: { - from_num: num_160 * 100, - from_frac: frac_160 * 10.0, - list_from_num: [num_160, num_160, num_160], - }, -} - -x_161 = 3.14 -y_161 = 1.23e45 -z_161 = 0.5 - -my_str_161 : Str -my_str_161 = "one" - -binops_161 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_161 : U64 -> U64 -add_one_161 = |n| n + 1 - -map_add_one_161 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_161 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_161 = |arg_one, arg_two| arg_one * arg_two - -num_161 = 42 -frac_161 = 4.2 -str_161 = "hello" - -# Polymorphic empty collections -empty_list_161 = [] - -# Mixed polymorphic structures -mixed_161 = { - numbers: { value: num_161, list: [num_161, num_161], float: frac }, - strings: { value: str_161, list: [str_161, str_161] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_161 }, - }, - computations: { - from_num: num_161 * 100, - from_frac: frac_161 * 10.0, - list_from_num: [num_161, num_161, num_161], - }, -} - -x_162 = 3.14 -y_162 = 1.23e45 -z_162 = 0.5 - -my_str_162 : Str -my_str_162 = "one" - -binops_162 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_162 : U64 -> U64 -add_one_162 = |n| n + 1 - -map_add_one_162 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_162 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_162 = |arg_one, arg_two| arg_one * arg_two - -num_162 = 42 -frac_162 = 4.2 -str_162 = "hello" - -# Polymorphic empty collections -empty_list_162 = [] - -# Mixed polymorphic structures -mixed_162 = { - numbers: { value: num_162, list: [num_162, num_162], float: frac }, - strings: { value: str_162, list: [str_162, str_162] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_162 }, - }, - computations: { - from_num: num_162 * 100, - from_frac: frac_162 * 10.0, - list_from_num: [num_162, num_162, num_162], - }, -} - -x_163 = 3.14 -y_163 = 1.23e45 -z_163 = 0.5 - -my_str_163 : Str -my_str_163 = "one" - -binops_163 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_163 : U64 -> U64 -add_one_163 = |n| n + 1 - -map_add_one_163 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_163 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_163 = |arg_one, arg_two| arg_one * arg_two - -num_163 = 42 -frac_163 = 4.2 -str_163 = "hello" - -# Polymorphic empty collections -empty_list_163 = [] - -# Mixed polymorphic structures -mixed_163 = { - numbers: { value: num_163, list: [num_163, num_163], float: frac }, - strings: { value: str_163, list: [str_163, str_163] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_163 }, - }, - computations: { - from_num: num_163 * 100, - from_frac: frac_163 * 10.0, - list_from_num: [num_163, num_163, num_163], - }, -} - -x_164 = 3.14 -y_164 = 1.23e45 -z_164 = 0.5 - -my_str_164 : Str -my_str_164 = "one" - -binops_164 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_164 : U64 -> U64 -add_one_164 = |n| n + 1 - -map_add_one_164 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_164 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_164 = |arg_one, arg_two| arg_one * arg_two - -num_164 = 42 -frac_164 = 4.2 -str_164 = "hello" - -# Polymorphic empty collections -empty_list_164 = [] - -# Mixed polymorphic structures -mixed_164 = { - numbers: { value: num_164, list: [num_164, num_164], float: frac }, - strings: { value: str_164, list: [str_164, str_164] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_164 }, - }, - computations: { - from_num: num_164 * 100, - from_frac: frac_164 * 10.0, - list_from_num: [num_164, num_164, num_164], - }, -} - -x_165 = 3.14 -y_165 = 1.23e45 -z_165 = 0.5 - -my_str_165 : Str -my_str_165 = "one" - -binops_165 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_165 : U64 -> U64 -add_one_165 = |n| n + 1 - -map_add_one_165 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_165 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_165 = |arg_one, arg_two| arg_one * arg_two - -num_165 = 42 -frac_165 = 4.2 -str_165 = "hello" - -# Polymorphic empty collections -empty_list_165 = [] - -# Mixed polymorphic structures -mixed_165 = { - numbers: { value: num_165, list: [num_165, num_165], float: frac }, - strings: { value: str_165, list: [str_165, str_165] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_165 }, - }, - computations: { - from_num: num_165 * 100, - from_frac: frac_165 * 10.0, - list_from_num: [num_165, num_165, num_165], - }, -} - -x_166 = 3.14 -y_166 = 1.23e45 -z_166 = 0.5 - -my_str_166 : Str -my_str_166 = "one" - -binops_166 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_166 : U64 -> U64 -add_one_166 = |n| n + 1 - -map_add_one_166 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_166 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_166 = |arg_one, arg_two| arg_one * arg_two - -num_166 = 42 -frac_166 = 4.2 -str_166 = "hello" - -# Polymorphic empty collections -empty_list_166 = [] - -# Mixed polymorphic structures -mixed_166 = { - numbers: { value: num_166, list: [num_166, num_166], float: frac }, - strings: { value: str_166, list: [str_166, str_166] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_166 }, - }, - computations: { - from_num: num_166 * 100, - from_frac: frac_166 * 10.0, - list_from_num: [num_166, num_166, num_166], - }, -} - -x_167 = 3.14 -y_167 = 1.23e45 -z_167 = 0.5 - -my_str_167 : Str -my_str_167 = "one" - -binops_167 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_167 : U64 -> U64 -add_one_167 = |n| n + 1 - -map_add_one_167 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_167 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_167 = |arg_one, arg_two| arg_one * arg_two - -num_167 = 42 -frac_167 = 4.2 -str_167 = "hello" - -# Polymorphic empty collections -empty_list_167 = [] - -# Mixed polymorphic structures -mixed_167 = { - numbers: { value: num_167, list: [num_167, num_167], float: frac }, - strings: { value: str_167, list: [str_167, str_167] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_167 }, - }, - computations: { - from_num: num_167 * 100, - from_frac: frac_167 * 10.0, - list_from_num: [num_167, num_167, num_167], - }, -} - -x_168 = 3.14 -y_168 = 1.23e45 -z_168 = 0.5 - -my_str_168 : Str -my_str_168 = "one" - -binops_168 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_168 : U64 -> U64 -add_one_168 = |n| n + 1 - -map_add_one_168 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_168 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_168 = |arg_one, arg_two| arg_one * arg_two - -num_168 = 42 -frac_168 = 4.2 -str_168 = "hello" - -# Polymorphic empty collections -empty_list_168 = [] - -# Mixed polymorphic structures -mixed_168 = { - numbers: { value: num_168, list: [num_168, num_168], float: frac }, - strings: { value: str_168, list: [str_168, str_168] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_168 }, - }, - computations: { - from_num: num_168 * 100, - from_frac: frac_168 * 10.0, - list_from_num: [num_168, num_168, num_168], - }, -} - -x_169 = 3.14 -y_169 = 1.23e45 -z_169 = 0.5 - -my_str_169 : Str -my_str_169 = "one" - -binops_169 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_169 : U64 -> U64 -add_one_169 = |n| n + 1 - -map_add_one_169 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_169 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_169 = |arg_one, arg_two| arg_one * arg_two - -num_169 = 42 -frac_169 = 4.2 -str_169 = "hello" - -# Polymorphic empty collections -empty_list_169 = [] - -# Mixed polymorphic structures -mixed_169 = { - numbers: { value: num_169, list: [num_169, num_169], float: frac }, - strings: { value: str_169, list: [str_169, str_169] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_169 }, - }, - computations: { - from_num: num_169 * 100, - from_frac: frac_169 * 10.0, - list_from_num: [num_169, num_169, num_169], - }, -} - -x_170 = 3.14 -y_170 = 1.23e45 -z_170 = 0.5 - -my_str_170 : Str -my_str_170 = "one" - -binops_170 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_170 : U64 -> U64 -add_one_170 = |n| n + 1 - -map_add_one_170 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_170 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_170 = |arg_one, arg_two| arg_one * arg_two - -num_170 = 42 -frac_170 = 4.2 -str_170 = "hello" - -# Polymorphic empty collections -empty_list_170 = [] - -# Mixed polymorphic structures -mixed_170 = { - numbers: { value: num_170, list: [num_170, num_170], float: frac }, - strings: { value: str_170, list: [str_170, str_170] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_170 }, - }, - computations: { - from_num: num_170 * 100, - from_frac: frac_170 * 10.0, - list_from_num: [num_170, num_170, num_170], - }, -} - -x_171 = 3.14 -y_171 = 1.23e45 -z_171 = 0.5 - -my_str_171 : Str -my_str_171 = "one" - -binops_171 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_171 : U64 -> U64 -add_one_171 = |n| n + 1 - -map_add_one_171 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_171 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_171 = |arg_one, arg_two| arg_one * arg_two - -num_171 = 42 -frac_171 = 4.2 -str_171 = "hello" - -# Polymorphic empty collections -empty_list_171 = [] - -# Mixed polymorphic structures -mixed_171 = { - numbers: { value: num_171, list: [num_171, num_171], float: frac }, - strings: { value: str_171, list: [str_171, str_171] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_171 }, - }, - computations: { - from_num: num_171 * 100, - from_frac: frac_171 * 10.0, - list_from_num: [num_171, num_171, num_171], - }, -} - -x_172 = 3.14 -y_172 = 1.23e45 -z_172 = 0.5 - -my_str_172 : Str -my_str_172 = "one" - -binops_172 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_172 : U64 -> U64 -add_one_172 = |n| n + 1 - -map_add_one_172 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_172 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_172 = |arg_one, arg_two| arg_one * arg_two - -num_172 = 42 -frac_172 = 4.2 -str_172 = "hello" - -# Polymorphic empty collections -empty_list_172 = [] - -# Mixed polymorphic structures -mixed_172 = { - numbers: { value: num_172, list: [num_172, num_172], float: frac }, - strings: { value: str_172, list: [str_172, str_172] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_172 }, - }, - computations: { - from_num: num_172 * 100, - from_frac: frac_172 * 10.0, - list_from_num: [num_172, num_172, num_172], - }, -} - -x_173 = 3.14 -y_173 = 1.23e45 -z_173 = 0.5 - -my_str_173 : Str -my_str_173 = "one" - -binops_173 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_173 : U64 -> U64 -add_one_173 = |n| n + 1 - -map_add_one_173 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_173 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_173 = |arg_one, arg_two| arg_one * arg_two - -num_173 = 42 -frac_173 = 4.2 -str_173 = "hello" - -# Polymorphic empty collections -empty_list_173 = [] - -# Mixed polymorphic structures -mixed_173 = { - numbers: { value: num_173, list: [num_173, num_173], float: frac }, - strings: { value: str_173, list: [str_173, str_173] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_173 }, - }, - computations: { - from_num: num_173 * 100, - from_frac: frac_173 * 10.0, - list_from_num: [num_173, num_173, num_173], - }, -} - -x_174 = 3.14 -y_174 = 1.23e45 -z_174 = 0.5 - -my_str_174 : Str -my_str_174 = "one" - -binops_174 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_174 : U64 -> U64 -add_one_174 = |n| n + 1 - -map_add_one_174 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_174 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_174 = |arg_one, arg_two| arg_one * arg_two - -num_174 = 42 -frac_174 = 4.2 -str_174 = "hello" - -# Polymorphic empty collections -empty_list_174 = [] - -# Mixed polymorphic structures -mixed_174 = { - numbers: { value: num_174, list: [num_174, num_174], float: frac }, - strings: { value: str_174, list: [str_174, str_174] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_174 }, - }, - computations: { - from_num: num_174 * 100, - from_frac: frac_174 * 10.0, - list_from_num: [num_174, num_174, num_174], - }, -} - -x_175 = 3.14 -y_175 = 1.23e45 -z_175 = 0.5 - -my_str_175 : Str -my_str_175 = "one" - -binops_175 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_175 : U64 -> U64 -add_one_175 = |n| n + 1 - -map_add_one_175 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_175 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_175 = |arg_one, arg_two| arg_one * arg_two - -num_175 = 42 -frac_175 = 4.2 -str_175 = "hello" - -# Polymorphic empty collections -empty_list_175 = [] - -# Mixed polymorphic structures -mixed_175 = { - numbers: { value: num_175, list: [num_175, num_175], float: frac }, - strings: { value: str_175, list: [str_175, str_175] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_175 }, - }, - computations: { - from_num: num_175 * 100, - from_frac: frac_175 * 10.0, - list_from_num: [num_175, num_175, num_175], - }, -} - -x_176 = 3.14 -y_176 = 1.23e45 -z_176 = 0.5 - -my_str_176 : Str -my_str_176 = "one" - -binops_176 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_176 : U64 -> U64 -add_one_176 = |n| n + 1 - -map_add_one_176 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_176 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_176 = |arg_one, arg_two| arg_one * arg_two - -num_176 = 42 -frac_176 = 4.2 -str_176 = "hello" - -# Polymorphic empty collections -empty_list_176 = [] - -# Mixed polymorphic structures -mixed_176 = { - numbers: { value: num_176, list: [num_176, num_176], float: frac }, - strings: { value: str_176, list: [str_176, str_176] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_176 }, - }, - computations: { - from_num: num_176 * 100, - from_frac: frac_176 * 10.0, - list_from_num: [num_176, num_176, num_176], - }, -} - -x_177 = 3.14 -y_177 = 1.23e45 -z_177 = 0.5 - -my_str_177 : Str -my_str_177 = "one" - -binops_177 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_177 : U64 -> U64 -add_one_177 = |n| n + 1 - -map_add_one_177 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_177 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_177 = |arg_one, arg_two| arg_one * arg_two - -num_177 = 42 -frac_177 = 4.2 -str_177 = "hello" - -# Polymorphic empty collections -empty_list_177 = [] - -# Mixed polymorphic structures -mixed_177 = { - numbers: { value: num_177, list: [num_177, num_177], float: frac }, - strings: { value: str_177, list: [str_177, str_177] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_177 }, - }, - computations: { - from_num: num_177 * 100, - from_frac: frac_177 * 10.0, - list_from_num: [num_177, num_177, num_177], - }, -} - -x_178 = 3.14 -y_178 = 1.23e45 -z_178 = 0.5 - -my_str_178 : Str -my_str_178 = "one" - -binops_178 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_178 : U64 -> U64 -add_one_178 = |n| n + 1 - -map_add_one_178 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_178 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_178 = |arg_one, arg_two| arg_one * arg_two - -num_178 = 42 -frac_178 = 4.2 -str_178 = "hello" - -# Polymorphic empty collections -empty_list_178 = [] - -# Mixed polymorphic structures -mixed_178 = { - numbers: { value: num_178, list: [num_178, num_178], float: frac }, - strings: { value: str_178, list: [str_178, str_178] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_178 }, - }, - computations: { - from_num: num_178 * 100, - from_frac: frac_178 * 10.0, - list_from_num: [num_178, num_178, num_178], - }, -} - -x_179 = 3.14 -y_179 = 1.23e45 -z_179 = 0.5 - -my_str_179 : Str -my_str_179 = "one" - -binops_179 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_179 : U64 -> U64 -add_one_179 = |n| n + 1 - -map_add_one_179 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_179 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_179 = |arg_one, arg_two| arg_one * arg_two - -num_179 = 42 -frac_179 = 4.2 -str_179 = "hello" - -# Polymorphic empty collections -empty_list_179 = [] - -# Mixed polymorphic structures -mixed_179 = { - numbers: { value: num_179, list: [num_179, num_179], float: frac }, - strings: { value: str_179, list: [str_179, str_179] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_179 }, - }, - computations: { - from_num: num_179 * 100, - from_frac: frac_179 * 10.0, - list_from_num: [num_179, num_179, num_179], - }, -} - -x_180 = 3.14 -y_180 = 1.23e45 -z_180 = 0.5 - -my_str_180 : Str -my_str_180 = "one" - -binops_180 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_180 : U64 -> U64 -add_one_180 = |n| n + 1 - -map_add_one_180 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_180 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_180 = |arg_one, arg_two| arg_one * arg_two - -num_180 = 42 -frac_180 = 4.2 -str_180 = "hello" - -# Polymorphic empty collections -empty_list_180 = [] - -# Mixed polymorphic structures -mixed_180 = { - numbers: { value: num_180, list: [num_180, num_180], float: frac }, - strings: { value: str_180, list: [str_180, str_180] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_180 }, - }, - computations: { - from_num: num_180 * 100, - from_frac: frac_180 * 10.0, - list_from_num: [num_180, num_180, num_180], - }, -} - -x_181 = 3.14 -y_181 = 1.23e45 -z_181 = 0.5 - -my_str_181 : Str -my_str_181 = "one" - -binops_181 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_181 : U64 -> U64 -add_one_181 = |n| n + 1 - -map_add_one_181 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_181 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_181 = |arg_one, arg_two| arg_one * arg_two - -num_181 = 42 -frac_181 = 4.2 -str_181 = "hello" - -# Polymorphic empty collections -empty_list_181 = [] - -# Mixed polymorphic structures -mixed_181 = { - numbers: { value: num_181, list: [num_181, num_181], float: frac }, - strings: { value: str_181, list: [str_181, str_181] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_181 }, - }, - computations: { - from_num: num_181 * 100, - from_frac: frac_181 * 10.0, - list_from_num: [num_181, num_181, num_181], - }, -} - -x_182 = 3.14 -y_182 = 1.23e45 -z_182 = 0.5 - -my_str_182 : Str -my_str_182 = "one" - -binops_182 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_182 : U64 -> U64 -add_one_182 = |n| n + 1 - -map_add_one_182 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_182 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_182 = |arg_one, arg_two| arg_one * arg_two - -num_182 = 42 -frac_182 = 4.2 -str_182 = "hello" - -# Polymorphic empty collections -empty_list_182 = [] - -# Mixed polymorphic structures -mixed_182 = { - numbers: { value: num_182, list: [num_182, num_182], float: frac }, - strings: { value: str_182, list: [str_182, str_182] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_182 }, - }, - computations: { - from_num: num_182 * 100, - from_frac: frac_182 * 10.0, - list_from_num: [num_182, num_182, num_182], - }, -} - -x_183 = 3.14 -y_183 = 1.23e45 -z_183 = 0.5 - -my_str_183 : Str -my_str_183 = "one" - -binops_183 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_183 : U64 -> U64 -add_one_183 = |n| n + 1 - -map_add_one_183 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_183 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_183 = |arg_one, arg_two| arg_one * arg_two - -num_183 = 42 -frac_183 = 4.2 -str_183 = "hello" - -# Polymorphic empty collections -empty_list_183 = [] - -# Mixed polymorphic structures -mixed_183 = { - numbers: { value: num_183, list: [num_183, num_183], float: frac }, - strings: { value: str_183, list: [str_183, str_183] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_183 }, - }, - computations: { - from_num: num_183 * 100, - from_frac: frac_183 * 10.0, - list_from_num: [num_183, num_183, num_183], - }, -} - -x_184 = 3.14 -y_184 = 1.23e45 -z_184 = 0.5 - -my_str_184 : Str -my_str_184 = "one" - -binops_184 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_184 : U64 -> U64 -add_one_184 = |n| n + 1 - -map_add_one_184 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_184 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_184 = |arg_one, arg_two| arg_one * arg_two - -num_184 = 42 -frac_184 = 4.2 -str_184 = "hello" - -# Polymorphic empty collections -empty_list_184 = [] - -# Mixed polymorphic structures -mixed_184 = { - numbers: { value: num_184, list: [num_184, num_184], float: frac }, - strings: { value: str_184, list: [str_184, str_184] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_184 }, - }, - computations: { - from_num: num_184 * 100, - from_frac: frac_184 * 10.0, - list_from_num: [num_184, num_184, num_184], - }, -} - -x_185 = 3.14 -y_185 = 1.23e45 -z_185 = 0.5 - -my_str_185 : Str -my_str_185 = "one" - -binops_185 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_185 : U64 -> U64 -add_one_185 = |n| n + 1 - -map_add_one_185 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_185 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_185 = |arg_one, arg_two| arg_one * arg_two - -num_185 = 42 -frac_185 = 4.2 -str_185 = "hello" - -# Polymorphic empty collections -empty_list_185 = [] - -# Mixed polymorphic structures -mixed_185 = { - numbers: { value: num_185, list: [num_185, num_185], float: frac }, - strings: { value: str_185, list: [str_185, str_185] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_185 }, - }, - computations: { - from_num: num_185 * 100, - from_frac: frac_185 * 10.0, - list_from_num: [num_185, num_185, num_185], - }, -} - -x_186 = 3.14 -y_186 = 1.23e45 -z_186 = 0.5 - -my_str_186 : Str -my_str_186 = "one" - -binops_186 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_186 : U64 -> U64 -add_one_186 = |n| n + 1 - -map_add_one_186 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_186 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_186 = |arg_one, arg_two| arg_one * arg_two - -num_186 = 42 -frac_186 = 4.2 -str_186 = "hello" - -# Polymorphic empty collections -empty_list_186 = [] - -# Mixed polymorphic structures -mixed_186 = { - numbers: { value: num_186, list: [num_186, num_186], float: frac }, - strings: { value: str_186, list: [str_186, str_186] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_186 }, - }, - computations: { - from_num: num_186 * 100, - from_frac: frac_186 * 10.0, - list_from_num: [num_186, num_186, num_186], - }, -} - -x_187 = 3.14 -y_187 = 1.23e45 -z_187 = 0.5 - -my_str_187 : Str -my_str_187 = "one" - -binops_187 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_187 : U64 -> U64 -add_one_187 = |n| n + 1 - -map_add_one_187 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_187 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_187 = |arg_one, arg_two| arg_one * arg_two - -num_187 = 42 -frac_187 = 4.2 -str_187 = "hello" - -# Polymorphic empty collections -empty_list_187 = [] - -# Mixed polymorphic structures -mixed_187 = { - numbers: { value: num_187, list: [num_187, num_187], float: frac }, - strings: { value: str_187, list: [str_187, str_187] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_187 }, - }, - computations: { - from_num: num_187 * 100, - from_frac: frac_187 * 10.0, - list_from_num: [num_187, num_187, num_187], - }, -} - -x_188 = 3.14 -y_188 = 1.23e45 -z_188 = 0.5 - -my_str_188 : Str -my_str_188 = "one" - -binops_188 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_188 : U64 -> U64 -add_one_188 = |n| n + 1 - -map_add_one_188 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_188 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_188 = |arg_one, arg_two| arg_one * arg_two - -num_188 = 42 -frac_188 = 4.2 -str_188 = "hello" - -# Polymorphic empty collections -empty_list_188 = [] - -# Mixed polymorphic structures -mixed_188 = { - numbers: { value: num_188, list: [num_188, num_188], float: frac }, - strings: { value: str_188, list: [str_188, str_188] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_188 }, - }, - computations: { - from_num: num_188 * 100, - from_frac: frac_188 * 10.0, - list_from_num: [num_188, num_188, num_188], - }, -} - -x_189 = 3.14 -y_189 = 1.23e45 -z_189 = 0.5 - -my_str_189 : Str -my_str_189 = "one" - -binops_189 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_189 : U64 -> U64 -add_one_189 = |n| n + 1 - -map_add_one_189 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_189 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_189 = |arg_one, arg_two| arg_one * arg_two - -num_189 = 42 -frac_189 = 4.2 -str_189 = "hello" - -# Polymorphic empty collections -empty_list_189 = [] - -# Mixed polymorphic structures -mixed_189 = { - numbers: { value: num_189, list: [num_189, num_189], float: frac }, - strings: { value: str_189, list: [str_189, str_189] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_189 }, - }, - computations: { - from_num: num_189 * 100, - from_frac: frac_189 * 10.0, - list_from_num: [num_189, num_189, num_189], - }, -} - -x_190 = 3.14 -y_190 = 1.23e45 -z_190 = 0.5 - -my_str_190 : Str -my_str_190 = "one" - -binops_190 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_190 : U64 -> U64 -add_one_190 = |n| n + 1 - -map_add_one_190 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_190 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_190 = |arg_one, arg_two| arg_one * arg_two - -num_190 = 42 -frac_190 = 4.2 -str_190 = "hello" - -# Polymorphic empty collections -empty_list_190 = [] - -# Mixed polymorphic structures -mixed_190 = { - numbers: { value: num_190, list: [num_190, num_190], float: frac }, - strings: { value: str_190, list: [str_190, str_190] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_190 }, - }, - computations: { - from_num: num_190 * 100, - from_frac: frac_190 * 10.0, - list_from_num: [num_190, num_190, num_190], - }, -} - -x_191 = 3.14 -y_191 = 1.23e45 -z_191 = 0.5 - -my_str_191 : Str -my_str_191 = "one" - -binops_191 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_191 : U64 -> U64 -add_one_191 = |n| n + 1 - -map_add_one_191 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_191 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_191 = |arg_one, arg_two| arg_one * arg_two - -num_191 = 42 -frac_191 = 4.2 -str_191 = "hello" - -# Polymorphic empty collections -empty_list_191 = [] - -# Mixed polymorphic structures -mixed_191 = { - numbers: { value: num_191, list: [num_191, num_191], float: frac }, - strings: { value: str_191, list: [str_191, str_191] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_191 }, - }, - computations: { - from_num: num_191 * 100, - from_frac: frac_191 * 10.0, - list_from_num: [num_191, num_191, num_191], - }, -} - -x_192 = 3.14 -y_192 = 1.23e45 -z_192 = 0.5 - -my_str_192 : Str -my_str_192 = "one" - -binops_192 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_192 : U64 -> U64 -add_one_192 = |n| n + 1 - -map_add_one_192 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_192 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_192 = |arg_one, arg_two| arg_one * arg_two - -num_192 = 42 -frac_192 = 4.2 -str_192 = "hello" - -# Polymorphic empty collections -empty_list_192 = [] - -# Mixed polymorphic structures -mixed_192 = { - numbers: { value: num_192, list: [num_192, num_192], float: frac }, - strings: { value: str_192, list: [str_192, str_192] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_192 }, - }, - computations: { - from_num: num_192 * 100, - from_frac: frac_192 * 10.0, - list_from_num: [num_192, num_192, num_192], - }, -} - -x_193 = 3.14 -y_193 = 1.23e45 -z_193 = 0.5 - -my_str_193 : Str -my_str_193 = "one" - -binops_193 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_193 : U64 -> U64 -add_one_193 = |n| n + 1 - -map_add_one_193 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_193 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_193 = |arg_one, arg_two| arg_one * arg_two - -num_193 = 42 -frac_193 = 4.2 -str_193 = "hello" - -# Polymorphic empty collections -empty_list_193 = [] - -# Mixed polymorphic structures -mixed_193 = { - numbers: { value: num_193, list: [num_193, num_193], float: frac }, - strings: { value: str_193, list: [str_193, str_193] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_193 }, - }, - computations: { - from_num: num_193 * 100, - from_frac: frac_193 * 10.0, - list_from_num: [num_193, num_193, num_193], - }, -} - -x_194 = 3.14 -y_194 = 1.23e45 -z_194 = 0.5 - -my_str_194 : Str -my_str_194 = "one" - -binops_194 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_194 : U64 -> U64 -add_one_194 = |n| n + 1 - -map_add_one_194 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_194 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_194 = |arg_one, arg_two| arg_one * arg_two - -num_194 = 42 -frac_194 = 4.2 -str_194 = "hello" - -# Polymorphic empty collections -empty_list_194 = [] - -# Mixed polymorphic structures -mixed_194 = { - numbers: { value: num_194, list: [num_194, num_194], float: frac }, - strings: { value: str_194, list: [str_194, str_194] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_194 }, - }, - computations: { - from_num: num_194 * 100, - from_frac: frac_194 * 10.0, - list_from_num: [num_194, num_194, num_194], - }, -} - -x_195 = 3.14 -y_195 = 1.23e45 -z_195 = 0.5 - -my_str_195 : Str -my_str_195 = "one" - -binops_195 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_195 : U64 -> U64 -add_one_195 = |n| n + 1 - -map_add_one_195 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_195 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_195 = |arg_one, arg_two| arg_one * arg_two - -num_195 = 42 -frac_195 = 4.2 -str_195 = "hello" - -# Polymorphic empty collections -empty_list_195 = [] - -# Mixed polymorphic structures -mixed_195 = { - numbers: { value: num_195, list: [num_195, num_195], float: frac }, - strings: { value: str_195, list: [str_195, str_195] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_195 }, - }, - computations: { - from_num: num_195 * 100, - from_frac: frac_195 * 10.0, - list_from_num: [num_195, num_195, num_195], - }, -} - -x_196 = 3.14 -y_196 = 1.23e45 -z_196 = 0.5 - -my_str_196 : Str -my_str_196 = "one" - -binops_196 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_196 : U64 -> U64 -add_one_196 = |n| n + 1 - -map_add_one_196 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_196 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_196 = |arg_one, arg_two| arg_one * arg_two - -num_196 = 42 -frac_196 = 4.2 -str_196 = "hello" - -# Polymorphic empty collections -empty_list_196 = [] - -# Mixed polymorphic structures -mixed_196 = { - numbers: { value: num_196, list: [num_196, num_196], float: frac }, - strings: { value: str_196, list: [str_196, str_196] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_196 }, - }, - computations: { - from_num: num_196 * 100, - from_frac: frac_196 * 10.0, - list_from_num: [num_196, num_196, num_196], - }, -} - -x_197 = 3.14 -y_197 = 1.23e45 -z_197 = 0.5 - -my_str_197 : Str -my_str_197 = "one" - -binops_197 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_197 : U64 -> U64 -add_one_197 = |n| n + 1 - -map_add_one_197 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_197 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_197 = |arg_one, arg_two| arg_one * arg_two - -num_197 = 42 -frac_197 = 4.2 -str_197 = "hello" - -# Polymorphic empty collections -empty_list_197 = [] - -# Mixed polymorphic structures -mixed_197 = { - numbers: { value: num_197, list: [num_197, num_197], float: frac }, - strings: { value: str_197, list: [str_197, str_197] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_197 }, - }, - computations: { - from_num: num_197 * 100, - from_frac: frac_197 * 10.0, - list_from_num: [num_197, num_197, num_197], - }, -} - -x_198 = 3.14 -y_198 = 1.23e45 -z_198 = 0.5 - -my_str_198 : Str -my_str_198 = "one" - -binops_198 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_198 : U64 -> U64 -add_one_198 = |n| n + 1 - -map_add_one_198 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_198 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_198 = |arg_one, arg_two| arg_one * arg_two - -num_198 = 42 -frac_198 = 4.2 -str_198 = "hello" - -# Polymorphic empty collections -empty_list_198 = [] - -# Mixed polymorphic structures -mixed_198 = { - numbers: { value: num_198, list: [num_198, num_198], float: frac }, - strings: { value: str_198, list: [str_198, str_198] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_198 }, - }, - computations: { - from_num: num_198 * 100, - from_frac: frac_198 * 10.0, - list_from_num: [num_198, num_198, num_198], - }, -} - -x_199 = 3.14 -y_199 = 1.23e45 -z_199 = 0.5 - -my_str_199 : Str -my_str_199 = "one" - -binops_199 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_199 : U64 -> U64 -add_one_199 = |n| n + 1 - -map_add_one_199 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_199 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_199 = |arg_one, arg_two| arg_one * arg_two - -num_199 = 42 -frac_199 = 4.2 -str_199 = "hello" - -# Polymorphic empty collections -empty_list_199 = [] - -# Mixed polymorphic structures -mixed_199 = { - numbers: { value: num_199, list: [num_199, num_199], float: frac }, - strings: { value: str_199, list: [str_199, str_199] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_199 }, - }, - computations: { - from_num: num_199 * 100, - from_frac: frac_199 * 10.0, - list_from_num: [num_199, num_199, num_199], - }, -} - -x_200 = 3.14 -y_200 = 1.23e45 -z_200 = 0.5 - -my_str_200 : Str -my_str_200 = "one" - -binops_200 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_200 : U64 -> U64 -add_one_200 = |n| n + 1 - -map_add_one_200 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_200 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_200 = |arg_one, arg_two| arg_one * arg_two - -num_200 = 42 -frac_200 = 4.2 -str_200 = "hello" - -# Polymorphic empty collections -empty_list_200 = [] - -# Mixed polymorphic structures -mixed_200 = { - numbers: { value: num_200, list: [num_200, num_200], float: frac }, - strings: { value: str_200, list: [str_200, str_200] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_200 }, - }, - computations: { - from_num: num_200 * 100, - from_frac: frac_200 * 10.0, - list_from_num: [num_200, num_200, num_200], - }, -} - -x_201 = 3.14 -y_201 = 1.23e45 -z_201 = 0.5 - -my_str_201 : Str -my_str_201 = "one" - -binops_201 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_201 : U64 -> U64 -add_one_201 = |n| n + 1 - -map_add_one_201 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_201 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_201 = |arg_one, arg_two| arg_one * arg_two - -num_201 = 42 -frac_201 = 4.2 -str_201 = "hello" - -# Polymorphic empty collections -empty_list_201 = [] - -# Mixed polymorphic structures -mixed_201 = { - numbers: { value: num_201, list: [num_201, num_201], float: frac }, - strings: { value: str_201, list: [str_201, str_201] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_201 }, - }, - computations: { - from_num: num_201 * 100, - from_frac: frac_201 * 10.0, - list_from_num: [num_201, num_201, num_201], - }, -} - -x_202 = 3.14 -y_202 = 1.23e45 -z_202 = 0.5 - -my_str_202 : Str -my_str_202 = "one" - -binops_202 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_202 : U64 -> U64 -add_one_202 = |n| n + 1 - -map_add_one_202 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_202 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_202 = |arg_one, arg_two| arg_one * arg_two - -num_202 = 42 -frac_202 = 4.2 -str_202 = "hello" - -# Polymorphic empty collections -empty_list_202 = [] - -# Mixed polymorphic structures -mixed_202 = { - numbers: { value: num_202, list: [num_202, num_202], float: frac }, - strings: { value: str_202, list: [str_202, str_202] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_202 }, - }, - computations: { - from_num: num_202 * 100, - from_frac: frac_202 * 10.0, - list_from_num: [num_202, num_202, num_202], - }, -} - -x_203 = 3.14 -y_203 = 1.23e45 -z_203 = 0.5 - -my_str_203 : Str -my_str_203 = "one" - -binops_203 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_203 : U64 -> U64 -add_one_203 = |n| n + 1 - -map_add_one_203 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_203 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_203 = |arg_one, arg_two| arg_one * arg_two - -num_203 = 42 -frac_203 = 4.2 -str_203 = "hello" - -# Polymorphic empty collections -empty_list_203 = [] - -# Mixed polymorphic structures -mixed_203 = { - numbers: { value: num_203, list: [num_203, num_203], float: frac }, - strings: { value: str_203, list: [str_203, str_203] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_203 }, - }, - computations: { - from_num: num_203 * 100, - from_frac: frac_203 * 10.0, - list_from_num: [num_203, num_203, num_203], - }, -} - -x_204 = 3.14 -y_204 = 1.23e45 -z_204 = 0.5 - -my_str_204 : Str -my_str_204 = "one" - -binops_204 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_204 : U64 -> U64 -add_one_204 = |n| n + 1 - -map_add_one_204 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_204 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_204 = |arg_one, arg_two| arg_one * arg_two - -num_204 = 42 -frac_204 = 4.2 -str_204 = "hello" - -# Polymorphic empty collections -empty_list_204 = [] - -# Mixed polymorphic structures -mixed_204 = { - numbers: { value: num_204, list: [num_204, num_204], float: frac }, - strings: { value: str_204, list: [str_204, str_204] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_204 }, - }, - computations: { - from_num: num_204 * 100, - from_frac: frac_204 * 10.0, - list_from_num: [num_204, num_204, num_204], - }, -} - -x_205 = 3.14 -y_205 = 1.23e45 -z_205 = 0.5 - -my_str_205 : Str -my_str_205 = "one" - -binops_205 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_205 : U64 -> U64 -add_one_205 = |n| n + 1 - -map_add_one_205 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_205 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_205 = |arg_one, arg_two| arg_one * arg_two - -num_205 = 42 -frac_205 = 4.2 -str_205 = "hello" - -# Polymorphic empty collections -empty_list_205 = [] - -# Mixed polymorphic structures -mixed_205 = { - numbers: { value: num_205, list: [num_205, num_205], float: frac }, - strings: { value: str_205, list: [str_205, str_205] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_205 }, - }, - computations: { - from_num: num_205 * 100, - from_frac: frac_205 * 10.0, - list_from_num: [num_205, num_205, num_205], - }, -} - -x_206 = 3.14 -y_206 = 1.23e45 -z_206 = 0.5 - -my_str_206 : Str -my_str_206 = "one" - -binops_206 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_206 : U64 -> U64 -add_one_206 = |n| n + 1 - -map_add_one_206 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_206 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_206 = |arg_one, arg_two| arg_one * arg_two - -num_206 = 42 -frac_206 = 4.2 -str_206 = "hello" - -# Polymorphic empty collections -empty_list_206 = [] - -# Mixed polymorphic structures -mixed_206 = { - numbers: { value: num_206, list: [num_206, num_206], float: frac }, - strings: { value: str_206, list: [str_206, str_206] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_206 }, - }, - computations: { - from_num: num_206 * 100, - from_frac: frac_206 * 10.0, - list_from_num: [num_206, num_206, num_206], - }, -} - -x_207 = 3.14 -y_207 = 1.23e45 -z_207 = 0.5 - -my_str_207 : Str -my_str_207 = "one" - -binops_207 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_207 : U64 -> U64 -add_one_207 = |n| n + 1 - -map_add_one_207 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_207 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_207 = |arg_one, arg_two| arg_one * arg_two - -num_207 = 42 -frac_207 = 4.2 -str_207 = "hello" - -# Polymorphic empty collections -empty_list_207 = [] - -# Mixed polymorphic structures -mixed_207 = { - numbers: { value: num_207, list: [num_207, num_207], float: frac }, - strings: { value: str_207, list: [str_207, str_207] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_207 }, - }, - computations: { - from_num: num_207 * 100, - from_frac: frac_207 * 10.0, - list_from_num: [num_207, num_207, num_207], - }, -} - -x_208 = 3.14 -y_208 = 1.23e45 -z_208 = 0.5 - -my_str_208 : Str -my_str_208 = "one" - -binops_208 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_208 : U64 -> U64 -add_one_208 = |n| n + 1 - -map_add_one_208 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_208 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_208 = |arg_one, arg_two| arg_one * arg_two - -num_208 = 42 -frac_208 = 4.2 -str_208 = "hello" - -# Polymorphic empty collections -empty_list_208 = [] - -# Mixed polymorphic structures -mixed_208 = { - numbers: { value: num_208, list: [num_208, num_208], float: frac }, - strings: { value: str_208, list: [str_208, str_208] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_208 }, - }, - computations: { - from_num: num_208 * 100, - from_frac: frac_208 * 10.0, - list_from_num: [num_208, num_208, num_208], - }, -} - -x_209 = 3.14 -y_209 = 1.23e45 -z_209 = 0.5 - -my_str_209 : Str -my_str_209 = "one" - -binops_209 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_209 : U64 -> U64 -add_one_209 = |n| n + 1 - -map_add_one_209 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_209 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_209 = |arg_one, arg_two| arg_one * arg_two - -num_209 = 42 -frac_209 = 4.2 -str_209 = "hello" - -# Polymorphic empty collections -empty_list_209 = [] - -# Mixed polymorphic structures -mixed_209 = { - numbers: { value: num_209, list: [num_209, num_209], float: frac }, - strings: { value: str_209, list: [str_209, str_209] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_209 }, - }, - computations: { - from_num: num_209 * 100, - from_frac: frac_209 * 10.0, - list_from_num: [num_209, num_209, num_209], - }, -} - -x_210 = 3.14 -y_210 = 1.23e45 -z_210 = 0.5 - -my_str_210 : Str -my_str_210 = "one" - -binops_210 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_210 : U64 -> U64 -add_one_210 = |n| n + 1 - -map_add_one_210 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_210 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_210 = |arg_one, arg_two| arg_one * arg_two - -num_210 = 42 -frac_210 = 4.2 -str_210 = "hello" - -# Polymorphic empty collections -empty_list_210 = [] - -# Mixed polymorphic structures -mixed_210 = { - numbers: { value: num_210, list: [num_210, num_210], float: frac }, - strings: { value: str_210, list: [str_210, str_210] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_210 }, - }, - computations: { - from_num: num_210 * 100, - from_frac: frac_210 * 10.0, - list_from_num: [num_210, num_210, num_210], - }, -} - -x_211 = 3.14 -y_211 = 1.23e45 -z_211 = 0.5 - -my_str_211 : Str -my_str_211 = "one" - -binops_211 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_211 : U64 -> U64 -add_one_211 = |n| n + 1 - -map_add_one_211 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_211 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_211 = |arg_one, arg_two| arg_one * arg_two - -num_211 = 42 -frac_211 = 4.2 -str_211 = "hello" - -# Polymorphic empty collections -empty_list_211 = [] - -# Mixed polymorphic structures -mixed_211 = { - numbers: { value: num_211, list: [num_211, num_211], float: frac }, - strings: { value: str_211, list: [str_211, str_211] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_211 }, - }, - computations: { - from_num: num_211 * 100, - from_frac: frac_211 * 10.0, - list_from_num: [num_211, num_211, num_211], - }, -} - -x_212 = 3.14 -y_212 = 1.23e45 -z_212 = 0.5 - -my_str_212 : Str -my_str_212 = "one" - -binops_212 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_212 : U64 -> U64 -add_one_212 = |n| n + 1 - -map_add_one_212 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_212 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_212 = |arg_one, arg_two| arg_one * arg_two - -num_212 = 42 -frac_212 = 4.2 -str_212 = "hello" - -# Polymorphic empty collections -empty_list_212 = [] - -# Mixed polymorphic structures -mixed_212 = { - numbers: { value: num_212, list: [num_212, num_212], float: frac }, - strings: { value: str_212, list: [str_212, str_212] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_212 }, - }, - computations: { - from_num: num_212 * 100, - from_frac: frac_212 * 10.0, - list_from_num: [num_212, num_212, num_212], - }, -} - -x_213 = 3.14 -y_213 = 1.23e45 -z_213 = 0.5 - -my_str_213 : Str -my_str_213 = "one" - -binops_213 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_213 : U64 -> U64 -add_one_213 = |n| n + 1 - -map_add_one_213 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_213 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_213 = |arg_one, arg_two| arg_one * arg_two - -num_213 = 42 -frac_213 = 4.2 -str_213 = "hello" - -# Polymorphic empty collections -empty_list_213 = [] - -# Mixed polymorphic structures -mixed_213 = { - numbers: { value: num_213, list: [num_213, num_213], float: frac }, - strings: { value: str_213, list: [str_213, str_213] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_213 }, - }, - computations: { - from_num: num_213 * 100, - from_frac: frac_213 * 10.0, - list_from_num: [num_213, num_213, num_213], - }, -} - -x_214 = 3.14 -y_214 = 1.23e45 -z_214 = 0.5 - -my_str_214 : Str -my_str_214 = "one" - -binops_214 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_214 : U64 -> U64 -add_one_214 = |n| n + 1 - -map_add_one_214 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_214 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_214 = |arg_one, arg_two| arg_one * arg_two - -num_214 = 42 -frac_214 = 4.2 -str_214 = "hello" - -# Polymorphic empty collections -empty_list_214 = [] - -# Mixed polymorphic structures -mixed_214 = { - numbers: { value: num_214, list: [num_214, num_214], float: frac }, - strings: { value: str_214, list: [str_214, str_214] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_214 }, - }, - computations: { - from_num: num_214 * 100, - from_frac: frac_214 * 10.0, - list_from_num: [num_214, num_214, num_214], - }, -} - -x_215 = 3.14 -y_215 = 1.23e45 -z_215 = 0.5 - -my_str_215 : Str -my_str_215 = "one" - -binops_215 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_215 : U64 -> U64 -add_one_215 = |n| n + 1 - -map_add_one_215 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_215 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_215 = |arg_one, arg_two| arg_one * arg_two - -num_215 = 42 -frac_215 = 4.2 -str_215 = "hello" - -# Polymorphic empty collections -empty_list_215 = [] - -# Mixed polymorphic structures -mixed_215 = { - numbers: { value: num_215, list: [num_215, num_215], float: frac }, - strings: { value: str_215, list: [str_215, str_215] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_215 }, - }, - computations: { - from_num: num_215 * 100, - from_frac: frac_215 * 10.0, - list_from_num: [num_215, num_215, num_215], - }, -} - -x_216 = 3.14 -y_216 = 1.23e45 -z_216 = 0.5 - -my_str_216 : Str -my_str_216 = "one" - -binops_216 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_216 : U64 -> U64 -add_one_216 = |n| n + 1 - -map_add_one_216 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_216 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_216 = |arg_one, arg_two| arg_one * arg_two - -num_216 = 42 -frac_216 = 4.2 -str_216 = "hello" - -# Polymorphic empty collections -empty_list_216 = [] - -# Mixed polymorphic structures -mixed_216 = { - numbers: { value: num_216, list: [num_216, num_216], float: frac }, - strings: { value: str_216, list: [str_216, str_216] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_216 }, - }, - computations: { - from_num: num_216 * 100, - from_frac: frac_216 * 10.0, - list_from_num: [num_216, num_216, num_216], - }, -} - -x_217 = 3.14 -y_217 = 1.23e45 -z_217 = 0.5 - -my_str_217 : Str -my_str_217 = "one" - -binops_217 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_217 : U64 -> U64 -add_one_217 = |n| n + 1 - -map_add_one_217 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_217 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_217 = |arg_one, arg_two| arg_one * arg_two - -num_217 = 42 -frac_217 = 4.2 -str_217 = "hello" - -# Polymorphic empty collections -empty_list_217 = [] - -# Mixed polymorphic structures -mixed_217 = { - numbers: { value: num_217, list: [num_217, num_217], float: frac }, - strings: { value: str_217, list: [str_217, str_217] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_217 }, - }, - computations: { - from_num: num_217 * 100, - from_frac: frac_217 * 10.0, - list_from_num: [num_217, num_217, num_217], - }, -} - -x_218 = 3.14 -y_218 = 1.23e45 -z_218 = 0.5 - -my_str_218 : Str -my_str_218 = "one" - -binops_218 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_218 : U64 -> U64 -add_one_218 = |n| n + 1 - -map_add_one_218 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_218 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_218 = |arg_one, arg_two| arg_one * arg_two - -num_218 = 42 -frac_218 = 4.2 -str_218 = "hello" - -# Polymorphic empty collections -empty_list_218 = [] - -# Mixed polymorphic structures -mixed_218 = { - numbers: { value: num_218, list: [num_218, num_218], float: frac }, - strings: { value: str_218, list: [str_218, str_218] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_218 }, - }, - computations: { - from_num: num_218 * 100, - from_frac: frac_218 * 10.0, - list_from_num: [num_218, num_218, num_218], - }, -} - -x_219 = 3.14 -y_219 = 1.23e45 -z_219 = 0.5 - -my_str_219 : Str -my_str_219 = "one" - -binops_219 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_219 : U64 -> U64 -add_one_219 = |n| n + 1 - -map_add_one_219 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_219 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_219 = |arg_one, arg_two| arg_one * arg_two - -num_219 = 42 -frac_219 = 4.2 -str_219 = "hello" - -# Polymorphic empty collections -empty_list_219 = [] - -# Mixed polymorphic structures -mixed_219 = { - numbers: { value: num_219, list: [num_219, num_219], float: frac }, - strings: { value: str_219, list: [str_219, str_219] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_219 }, - }, - computations: { - from_num: num_219 * 100, - from_frac: frac_219 * 10.0, - list_from_num: [num_219, num_219, num_219], - }, -} - -x_220 = 3.14 -y_220 = 1.23e45 -z_220 = 0.5 - -my_str_220 : Str -my_str_220 = "one" - -binops_220 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_220 : U64 -> U64 -add_one_220 = |n| n + 1 - -map_add_one_220 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_220 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_220 = |arg_one, arg_two| arg_one * arg_two - -num_220 = 42 -frac_220 = 4.2 -str_220 = "hello" - -# Polymorphic empty collections -empty_list_220 = [] - -# Mixed polymorphic structures -mixed_220 = { - numbers: { value: num_220, list: [num_220, num_220], float: frac }, - strings: { value: str_220, list: [str_220, str_220] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_220 }, - }, - computations: { - from_num: num_220 * 100, - from_frac: frac_220 * 10.0, - list_from_num: [num_220, num_220, num_220], - }, -} - -x_221 = 3.14 -y_221 = 1.23e45 -z_221 = 0.5 - -my_str_221 : Str -my_str_221 = "one" - -binops_221 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_221 : U64 -> U64 -add_one_221 = |n| n + 1 - -map_add_one_221 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_221 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_221 = |arg_one, arg_two| arg_one * arg_two - -num_221 = 42 -frac_221 = 4.2 -str_221 = "hello" - -# Polymorphic empty collections -empty_list_221 = [] - -# Mixed polymorphic structures -mixed_221 = { - numbers: { value: num_221, list: [num_221, num_221], float: frac }, - strings: { value: str_221, list: [str_221, str_221] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_221 }, - }, - computations: { - from_num: num_221 * 100, - from_frac: frac_221 * 10.0, - list_from_num: [num_221, num_221, num_221], - }, -} - -x_222 = 3.14 -y_222 = 1.23e45 -z_222 = 0.5 - -my_str_222 : Str -my_str_222 = "one" - -binops_222 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_222 : U64 -> U64 -add_one_222 = |n| n + 1 - -map_add_one_222 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_222 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_222 = |arg_one, arg_two| arg_one * arg_two - -num_222 = 42 -frac_222 = 4.2 -str_222 = "hello" - -# Polymorphic empty collections -empty_list_222 = [] - -# Mixed polymorphic structures -mixed_222 = { - numbers: { value: num_222, list: [num_222, num_222], float: frac }, - strings: { value: str_222, list: [str_222, str_222] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_222 }, - }, - computations: { - from_num: num_222 * 100, - from_frac: frac_222 * 10.0, - list_from_num: [num_222, num_222, num_222], - }, -} - -x_223 = 3.14 -y_223 = 1.23e45 -z_223 = 0.5 - -my_str_223 : Str -my_str_223 = "one" - -binops_223 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_223 : U64 -> U64 -add_one_223 = |n| n + 1 - -map_add_one_223 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_223 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_223 = |arg_one, arg_two| arg_one * arg_two - -num_223 = 42 -frac_223 = 4.2 -str_223 = "hello" - -# Polymorphic empty collections -empty_list_223 = [] - -# Mixed polymorphic structures -mixed_223 = { - numbers: { value: num_223, list: [num_223, num_223], float: frac }, - strings: { value: str_223, list: [str_223, str_223] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_223 }, - }, - computations: { - from_num: num_223 * 100, - from_frac: frac_223 * 10.0, - list_from_num: [num_223, num_223, num_223], - }, -} - -x_224 = 3.14 -y_224 = 1.23e45 -z_224 = 0.5 - -my_str_224 : Str -my_str_224 = "one" - -binops_224 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_224 : U64 -> U64 -add_one_224 = |n| n + 1 - -map_add_one_224 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_224 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_224 = |arg_one, arg_two| arg_one * arg_two - -num_224 = 42 -frac_224 = 4.2 -str_224 = "hello" - -# Polymorphic empty collections -empty_list_224 = [] - -# Mixed polymorphic structures -mixed_224 = { - numbers: { value: num_224, list: [num_224, num_224], float: frac }, - strings: { value: str_224, list: [str_224, str_224] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_224 }, - }, - computations: { - from_num: num_224 * 100, - from_frac: frac_224 * 10.0, - list_from_num: [num_224, num_224, num_224], - }, -} - -x_225 = 3.14 -y_225 = 1.23e45 -z_225 = 0.5 - -my_str_225 : Str -my_str_225 = "one" - -binops_225 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_225 : U64 -> U64 -add_one_225 = |n| n + 1 - -map_add_one_225 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_225 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_225 = |arg_one, arg_two| arg_one * arg_two - -num_225 = 42 -frac_225 = 4.2 -str_225 = "hello" - -# Polymorphic empty collections -empty_list_225 = [] - -# Mixed polymorphic structures -mixed_225 = { - numbers: { value: num_225, list: [num_225, num_225], float: frac }, - strings: { value: str_225, list: [str_225, str_225] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_225 }, - }, - computations: { - from_num: num_225 * 100, - from_frac: frac_225 * 10.0, - list_from_num: [num_225, num_225, num_225], - }, -} - -x_226 = 3.14 -y_226 = 1.23e45 -z_226 = 0.5 - -my_str_226 : Str -my_str_226 = "one" - -binops_226 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_226 : U64 -> U64 -add_one_226 = |n| n + 1 - -map_add_one_226 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_226 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_226 = |arg_one, arg_two| arg_one * arg_two - -num_226 = 42 -frac_226 = 4.2 -str_226 = "hello" - -# Polymorphic empty collections -empty_list_226 = [] - -# Mixed polymorphic structures -mixed_226 = { - numbers: { value: num_226, list: [num_226, num_226], float: frac }, - strings: { value: str_226, list: [str_226, str_226] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_226 }, - }, - computations: { - from_num: num_226 * 100, - from_frac: frac_226 * 10.0, - list_from_num: [num_226, num_226, num_226], - }, -} - -x_227 = 3.14 -y_227 = 1.23e45 -z_227 = 0.5 - -my_str_227 : Str -my_str_227 = "one" - -binops_227 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_227 : U64 -> U64 -add_one_227 = |n| n + 1 - -map_add_one_227 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_227 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_227 = |arg_one, arg_two| arg_one * arg_two - -num_227 = 42 -frac_227 = 4.2 -str_227 = "hello" - -# Polymorphic empty collections -empty_list_227 = [] - -# Mixed polymorphic structures -mixed_227 = { - numbers: { value: num_227, list: [num_227, num_227], float: frac }, - strings: { value: str_227, list: [str_227, str_227] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_227 }, - }, - computations: { - from_num: num_227 * 100, - from_frac: frac_227 * 10.0, - list_from_num: [num_227, num_227, num_227], - }, -} - -x_228 = 3.14 -y_228 = 1.23e45 -z_228 = 0.5 - -my_str_228 : Str -my_str_228 = "one" - -binops_228 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_228 : U64 -> U64 -add_one_228 = |n| n + 1 - -map_add_one_228 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_228 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_228 = |arg_one, arg_two| arg_one * arg_two - -num_228 = 42 -frac_228 = 4.2 -str_228 = "hello" - -# Polymorphic empty collections -empty_list_228 = [] - -# Mixed polymorphic structures -mixed_228 = { - numbers: { value: num_228, list: [num_228, num_228], float: frac }, - strings: { value: str_228, list: [str_228, str_228] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_228 }, - }, - computations: { - from_num: num_228 * 100, - from_frac: frac_228 * 10.0, - list_from_num: [num_228, num_228, num_228], - }, -} - -x_229 = 3.14 -y_229 = 1.23e45 -z_229 = 0.5 - -my_str_229 : Str -my_str_229 = "one" - -binops_229 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_229 : U64 -> U64 -add_one_229 = |n| n + 1 - -map_add_one_229 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_229 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_229 = |arg_one, arg_two| arg_one * arg_two - -num_229 = 42 -frac_229 = 4.2 -str_229 = "hello" - -# Polymorphic empty collections -empty_list_229 = [] - -# Mixed polymorphic structures -mixed_229 = { - numbers: { value: num_229, list: [num_229, num_229], float: frac }, - strings: { value: str_229, list: [str_229, str_229] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_229 }, - }, - computations: { - from_num: num_229 * 100, - from_frac: frac_229 * 10.0, - list_from_num: [num_229, num_229, num_229], - }, -} - -x_230 = 3.14 -y_230 = 1.23e45 -z_230 = 0.5 - -my_str_230 : Str -my_str_230 = "one" - -binops_230 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_230 : U64 -> U64 -add_one_230 = |n| n + 1 - -map_add_one_230 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_230 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_230 = |arg_one, arg_two| arg_one * arg_two - -num_230 = 42 -frac_230 = 4.2 -str_230 = "hello" - -# Polymorphic empty collections -empty_list_230 = [] - -# Mixed polymorphic structures -mixed_230 = { - numbers: { value: num_230, list: [num_230, num_230], float: frac }, - strings: { value: str_230, list: [str_230, str_230] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_230 }, - }, - computations: { - from_num: num_230 * 100, - from_frac: frac_230 * 10.0, - list_from_num: [num_230, num_230, num_230], - }, -} - -x_231 = 3.14 -y_231 = 1.23e45 -z_231 = 0.5 - -my_str_231 : Str -my_str_231 = "one" - -binops_231 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_231 : U64 -> U64 -add_one_231 = |n| n + 1 - -map_add_one_231 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_231 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_231 = |arg_one, arg_two| arg_one * arg_two - -num_231 = 42 -frac_231 = 4.2 -str_231 = "hello" - -# Polymorphic empty collections -empty_list_231 = [] - -# Mixed polymorphic structures -mixed_231 = { - numbers: { value: num_231, list: [num_231, num_231], float: frac }, - strings: { value: str_231, list: [str_231, str_231] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_231 }, - }, - computations: { - from_num: num_231 * 100, - from_frac: frac_231 * 10.0, - list_from_num: [num_231, num_231, num_231], - }, -} - -x_232 = 3.14 -y_232 = 1.23e45 -z_232 = 0.5 - -my_str_232 : Str -my_str_232 = "one" - -binops_232 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_232 : U64 -> U64 -add_one_232 = |n| n + 1 - -map_add_one_232 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_232 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_232 = |arg_one, arg_two| arg_one * arg_two - -num_232 = 42 -frac_232 = 4.2 -str_232 = "hello" - -# Polymorphic empty collections -empty_list_232 = [] - -# Mixed polymorphic structures -mixed_232 = { - numbers: { value: num_232, list: [num_232, num_232], float: frac }, - strings: { value: str_232, list: [str_232, str_232] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_232 }, - }, - computations: { - from_num: num_232 * 100, - from_frac: frac_232 * 10.0, - list_from_num: [num_232, num_232, num_232], - }, -} - -x_233 = 3.14 -y_233 = 1.23e45 -z_233 = 0.5 - -my_str_233 : Str -my_str_233 = "one" - -binops_233 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_233 : U64 -> U64 -add_one_233 = |n| n + 1 - -map_add_one_233 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_233 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_233 = |arg_one, arg_two| arg_one * arg_two - -num_233 = 42 -frac_233 = 4.2 -str_233 = "hello" - -# Polymorphic empty collections -empty_list_233 = [] - -# Mixed polymorphic structures -mixed_233 = { - numbers: { value: num_233, list: [num_233, num_233], float: frac }, - strings: { value: str_233, list: [str_233, str_233] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_233 }, - }, - computations: { - from_num: num_233 * 100, - from_frac: frac_233 * 10.0, - list_from_num: [num_233, num_233, num_233], - }, -} - -x_234 = 3.14 -y_234 = 1.23e45 -z_234 = 0.5 - -my_str_234 : Str -my_str_234 = "one" - -binops_234 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_234 : U64 -> U64 -add_one_234 = |n| n + 1 - -map_add_one_234 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_234 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_234 = |arg_one, arg_two| arg_one * arg_two - -num_234 = 42 -frac_234 = 4.2 -str_234 = "hello" - -# Polymorphic empty collections -empty_list_234 = [] - -# Mixed polymorphic structures -mixed_234 = { - numbers: { value: num_234, list: [num_234, num_234], float: frac }, - strings: { value: str_234, list: [str_234, str_234] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_234 }, - }, - computations: { - from_num: num_234 * 100, - from_frac: frac_234 * 10.0, - list_from_num: [num_234, num_234, num_234], - }, -} - -x_235 = 3.14 -y_235 = 1.23e45 -z_235 = 0.5 - -my_str_235 : Str -my_str_235 = "one" - -binops_235 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_235 : U64 -> U64 -add_one_235 = |n| n + 1 - -map_add_one_235 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_235 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_235 = |arg_one, arg_two| arg_one * arg_two - -num_235 = 42 -frac_235 = 4.2 -str_235 = "hello" - -# Polymorphic empty collections -empty_list_235 = [] - -# Mixed polymorphic structures -mixed_235 = { - numbers: { value: num_235, list: [num_235, num_235], float: frac }, - strings: { value: str_235, list: [str_235, str_235] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_235 }, - }, - computations: { - from_num: num_235 * 100, - from_frac: frac_235 * 10.0, - list_from_num: [num_235, num_235, num_235], - }, -} - -x_236 = 3.14 -y_236 = 1.23e45 -z_236 = 0.5 - -my_str_236 : Str -my_str_236 = "one" - -binops_236 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_236 : U64 -> U64 -add_one_236 = |n| n + 1 - -map_add_one_236 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_236 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_236 = |arg_one, arg_two| arg_one * arg_two - -num_236 = 42 -frac_236 = 4.2 -str_236 = "hello" - -# Polymorphic empty collections -empty_list_236 = [] - -# Mixed polymorphic structures -mixed_236 = { - numbers: { value: num_236, list: [num_236, num_236], float: frac }, - strings: { value: str_236, list: [str_236, str_236] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_236 }, - }, - computations: { - from_num: num_236 * 100, - from_frac: frac_236 * 10.0, - list_from_num: [num_236, num_236, num_236], - }, -} - -x_237 = 3.14 -y_237 = 1.23e45 -z_237 = 0.5 - -my_str_237 : Str -my_str_237 = "one" - -binops_237 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_237 : U64 -> U64 -add_one_237 = |n| n + 1 - -map_add_one_237 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_237 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_237 = |arg_one, arg_two| arg_one * arg_two - -num_237 = 42 -frac_237 = 4.2 -str_237 = "hello" - -# Polymorphic empty collections -empty_list_237 = [] - -# Mixed polymorphic structures -mixed_237 = { - numbers: { value: num_237, list: [num_237, num_237], float: frac }, - strings: { value: str_237, list: [str_237, str_237] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_237 }, - }, - computations: { - from_num: num_237 * 100, - from_frac: frac_237 * 10.0, - list_from_num: [num_237, num_237, num_237], - }, -} - -x_238 = 3.14 -y_238 = 1.23e45 -z_238 = 0.5 - -my_str_238 : Str -my_str_238 = "one" - -binops_238 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_238 : U64 -> U64 -add_one_238 = |n| n + 1 - -map_add_one_238 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_238 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_238 = |arg_one, arg_two| arg_one * arg_two - -num_238 = 42 -frac_238 = 4.2 -str_238 = "hello" - -# Polymorphic empty collections -empty_list_238 = [] - -# Mixed polymorphic structures -mixed_238 = { - numbers: { value: num_238, list: [num_238, num_238], float: frac }, - strings: { value: str_238, list: [str_238, str_238] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_238 }, - }, - computations: { - from_num: num_238 * 100, - from_frac: frac_238 * 10.0, - list_from_num: [num_238, num_238, num_238], - }, -} - -x_239 = 3.14 -y_239 = 1.23e45 -z_239 = 0.5 - -my_str_239 : Str -my_str_239 = "one" - -binops_239 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_239 : U64 -> U64 -add_one_239 = |n| n + 1 - -map_add_one_239 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_239 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_239 = |arg_one, arg_two| arg_one * arg_two - -num_239 = 42 -frac_239 = 4.2 -str_239 = "hello" - -# Polymorphic empty collections -empty_list_239 = [] - -# Mixed polymorphic structures -mixed_239 = { - numbers: { value: num_239, list: [num_239, num_239], float: frac }, - strings: { value: str_239, list: [str_239, str_239] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_239 }, - }, - computations: { - from_num: num_239 * 100, - from_frac: frac_239 * 10.0, - list_from_num: [num_239, num_239, num_239], - }, -} - -x_240 = 3.14 -y_240 = 1.23e45 -z_240 = 0.5 - -my_str_240 : Str -my_str_240 = "one" - -binops_240 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_240 : U64 -> U64 -add_one_240 = |n| n + 1 - -map_add_one_240 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_240 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_240 = |arg_one, arg_two| arg_one * arg_two - -num_240 = 42 -frac_240 = 4.2 -str_240 = "hello" - -# Polymorphic empty collections -empty_list_240 = [] - -# Mixed polymorphic structures -mixed_240 = { - numbers: { value: num_240, list: [num_240, num_240], float: frac }, - strings: { value: str_240, list: [str_240, str_240] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_240 }, - }, - computations: { - from_num: num_240 * 100, - from_frac: frac_240 * 10.0, - list_from_num: [num_240, num_240, num_240], - }, -} - -x_241 = 3.14 -y_241 = 1.23e45 -z_241 = 0.5 - -my_str_241 : Str -my_str_241 = "one" - -binops_241 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_241 : U64 -> U64 -add_one_241 = |n| n + 1 - -map_add_one_241 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_241 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_241 = |arg_one, arg_two| arg_one * arg_two - -num_241 = 42 -frac_241 = 4.2 -str_241 = "hello" - -# Polymorphic empty collections -empty_list_241 = [] - -# Mixed polymorphic structures -mixed_241 = { - numbers: { value: num_241, list: [num_241, num_241], float: frac }, - strings: { value: str_241, list: [str_241, str_241] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_241 }, - }, - computations: { - from_num: num_241 * 100, - from_frac: frac_241 * 10.0, - list_from_num: [num_241, num_241, num_241], - }, -} - -x_242 = 3.14 -y_242 = 1.23e45 -z_242 = 0.5 - -my_str_242 : Str -my_str_242 = "one" - -binops_242 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_242 : U64 -> U64 -add_one_242 = |n| n + 1 - -map_add_one_242 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_242 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_242 = |arg_one, arg_two| arg_one * arg_two - -num_242 = 42 -frac_242 = 4.2 -str_242 = "hello" - -# Polymorphic empty collections -empty_list_242 = [] - -# Mixed polymorphic structures -mixed_242 = { - numbers: { value: num_242, list: [num_242, num_242], float: frac }, - strings: { value: str_242, list: [str_242, str_242] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_242 }, - }, - computations: { - from_num: num_242 * 100, - from_frac: frac_242 * 10.0, - list_from_num: [num_242, num_242, num_242], - }, -} - -x_243 = 3.14 -y_243 = 1.23e45 -z_243 = 0.5 - -my_str_243 : Str -my_str_243 = "one" - -binops_243 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_243 : U64 -> U64 -add_one_243 = |n| n + 1 - -map_add_one_243 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_243 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_243 = |arg_one, arg_two| arg_one * arg_two - -num_243 = 42 -frac_243 = 4.2 -str_243 = "hello" - -# Polymorphic empty collections -empty_list_243 = [] - -# Mixed polymorphic structures -mixed_243 = { - numbers: { value: num_243, list: [num_243, num_243], float: frac }, - strings: { value: str_243, list: [str_243, str_243] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_243 }, - }, - computations: { - from_num: num_243 * 100, - from_frac: frac_243 * 10.0, - list_from_num: [num_243, num_243, num_243], - }, -} - -x_244 = 3.14 -y_244 = 1.23e45 -z_244 = 0.5 - -my_str_244 : Str -my_str_244 = "one" - -binops_244 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_244 : U64 -> U64 -add_one_244 = |n| n + 1 - -map_add_one_244 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_244 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_244 = |arg_one, arg_two| arg_one * arg_two - -num_244 = 42 -frac_244 = 4.2 -str_244 = "hello" - -# Polymorphic empty collections -empty_list_244 = [] - -# Mixed polymorphic structures -mixed_244 = { - numbers: { value: num_244, list: [num_244, num_244], float: frac }, - strings: { value: str_244, list: [str_244, str_244] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_244 }, - }, - computations: { - from_num: num_244 * 100, - from_frac: frac_244 * 10.0, - list_from_num: [num_244, num_244, num_244], - }, -} - -x_245 = 3.14 -y_245 = 1.23e45 -z_245 = 0.5 - -my_str_245 : Str -my_str_245 = "one" - -binops_245 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_245 : U64 -> U64 -add_one_245 = |n| n + 1 - -map_add_one_245 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_245 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_245 = |arg_one, arg_two| arg_one * arg_two - -num_245 = 42 -frac_245 = 4.2 -str_245 = "hello" - -# Polymorphic empty collections -empty_list_245 = [] - -# Mixed polymorphic structures -mixed_245 = { - numbers: { value: num_245, list: [num_245, num_245], float: frac }, - strings: { value: str_245, list: [str_245, str_245] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_245 }, - }, - computations: { - from_num: num_245 * 100, - from_frac: frac_245 * 10.0, - list_from_num: [num_245, num_245, num_245], - }, -} - -x_246 = 3.14 -y_246 = 1.23e45 -z_246 = 0.5 - -my_str_246 : Str -my_str_246 = "one" - -binops_246 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_246 : U64 -> U64 -add_one_246 = |n| n + 1 - -map_add_one_246 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_246 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_246 = |arg_one, arg_two| arg_one * arg_two - -num_246 = 42 -frac_246 = 4.2 -str_246 = "hello" - -# Polymorphic empty collections -empty_list_246 = [] - -# Mixed polymorphic structures -mixed_246 = { - numbers: { value: num_246, list: [num_246, num_246], float: frac }, - strings: { value: str_246, list: [str_246, str_246] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_246 }, - }, - computations: { - from_num: num_246 * 100, - from_frac: frac_246 * 10.0, - list_from_num: [num_246, num_246, num_246], - }, -} - -x_247 = 3.14 -y_247 = 1.23e45 -z_247 = 0.5 - -my_str_247 : Str -my_str_247 = "one" - -binops_247 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_247 : U64 -> U64 -add_one_247 = |n| n + 1 - -map_add_one_247 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_247 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_247 = |arg_one, arg_two| arg_one * arg_two - -num_247 = 42 -frac_247 = 4.2 -str_247 = "hello" - -# Polymorphic empty collections -empty_list_247 = [] - -# Mixed polymorphic structures -mixed_247 = { - numbers: { value: num_247, list: [num_247, num_247], float: frac }, - strings: { value: str_247, list: [str_247, str_247] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_247 }, - }, - computations: { - from_num: num_247 * 100, - from_frac: frac_247 * 10.0, - list_from_num: [num_247, num_247, num_247], - }, -} - -x_248 = 3.14 -y_248 = 1.23e45 -z_248 = 0.5 - -my_str_248 : Str -my_str_248 = "one" - -binops_248 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_248 : U64 -> U64 -add_one_248 = |n| n + 1 - -map_add_one_248 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_248 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_248 = |arg_one, arg_two| arg_one * arg_two - -num_248 = 42 -frac_248 = 4.2 -str_248 = "hello" - -# Polymorphic empty collections -empty_list_248 = [] - -# Mixed polymorphic structures -mixed_248 = { - numbers: { value: num_248, list: [num_248, num_248], float: frac }, - strings: { value: str_248, list: [str_248, str_248] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_248 }, - }, - computations: { - from_num: num_248 * 100, - from_frac: frac_248 * 10.0, - list_from_num: [num_248, num_248, num_248], - }, -} - -x_249 = 3.14 -y_249 = 1.23e45 -z_249 = 0.5 - -my_str_249 : Str -my_str_249 = "one" - -binops_249 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_249 : U64 -> U64 -add_one_249 = |n| n + 1 - -map_add_one_249 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_249 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_249 = |arg_one, arg_two| arg_one * arg_two - -num_249 = 42 -frac_249 = 4.2 -str_249 = "hello" - -# Polymorphic empty collections -empty_list_249 = [] - -# Mixed polymorphic structures -mixed_249 = { - numbers: { value: num_249, list: [num_249, num_249], float: frac }, - strings: { value: str_249, list: [str_249, str_249] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_249 }, - }, - computations: { - from_num: num_249 * 100, - from_frac: frac_249 * 10.0, - list_from_num: [num_249, num_249, num_249], - }, -} - -x_250 = 3.14 -y_250 = 1.23e45 -z_250 = 0.5 - -my_str_250 : Str -my_str_250 = "one" - -binops_250 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_250 : U64 -> U64 -add_one_250 = |n| n + 1 - -map_add_one_250 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_250 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_250 = |arg_one, arg_two| arg_one * arg_two - -num_250 = 42 -frac_250 = 4.2 -str_250 = "hello" - -# Polymorphic empty collections -empty_list_250 = [] - -# Mixed polymorphic structures -mixed_250 = { - numbers: { value: num_250, list: [num_250, num_250], float: frac }, - strings: { value: str_250, list: [str_250, str_250] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_250 }, - }, - computations: { - from_num: num_250 * 100, - from_frac: frac_250 * 10.0, - list_from_num: [num_250, num_250, num_250], - }, -} - -x_251 = 3.14 -y_251 = 1.23e45 -z_251 = 0.5 - -my_str_251 : Str -my_str_251 = "one" - -binops_251 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_251 : U64 -> U64 -add_one_251 = |n| n + 1 - -map_add_one_251 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_251 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_251 = |arg_one, arg_two| arg_one * arg_two - -num_251 = 42 -frac_251 = 4.2 -str_251 = "hello" - -# Polymorphic empty collections -empty_list_251 = [] - -# Mixed polymorphic structures -mixed_251 = { - numbers: { value: num_251, list: [num_251, num_251], float: frac }, - strings: { value: str_251, list: [str_251, str_251] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_251 }, - }, - computations: { - from_num: num_251 * 100, - from_frac: frac_251 * 10.0, - list_from_num: [num_251, num_251, num_251], - }, -} - -x_252 = 3.14 -y_252 = 1.23e45 -z_252 = 0.5 - -my_str_252 : Str -my_str_252 = "one" - -binops_252 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_252 : U64 -> U64 -add_one_252 = |n| n + 1 - -map_add_one_252 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_252 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_252 = |arg_one, arg_two| arg_one * arg_two - -num_252 = 42 -frac_252 = 4.2 -str_252 = "hello" - -# Polymorphic empty collections -empty_list_252 = [] - -# Mixed polymorphic structures -mixed_252 = { - numbers: { value: num_252, list: [num_252, num_252], float: frac }, - strings: { value: str_252, list: [str_252, str_252] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_252 }, - }, - computations: { - from_num: num_252 * 100, - from_frac: frac_252 * 10.0, - list_from_num: [num_252, num_252, num_252], - }, -} - -x_253 = 3.14 -y_253 = 1.23e45 -z_253 = 0.5 - -my_str_253 : Str -my_str_253 = "one" - -binops_253 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_253 : U64 -> U64 -add_one_253 = |n| n + 1 - -map_add_one_253 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_253 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_253 = |arg_one, arg_two| arg_one * arg_two - -num_253 = 42 -frac_253 = 4.2 -str_253 = "hello" - -# Polymorphic empty collections -empty_list_253 = [] - -# Mixed polymorphic structures -mixed_253 = { - numbers: { value: num_253, list: [num_253, num_253], float: frac }, - strings: { value: str_253, list: [str_253, str_253] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_253 }, - }, - computations: { - from_num: num_253 * 100, - from_frac: frac_253 * 10.0, - list_from_num: [num_253, num_253, num_253], - }, -} - -x_254 = 3.14 -y_254 = 1.23e45 -z_254 = 0.5 - -my_str_254 : Str -my_str_254 = "one" - -binops_254 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_254 : U64 -> U64 -add_one_254 = |n| n + 1 - -map_add_one_254 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_254 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_254 = |arg_one, arg_two| arg_one * arg_two - -num_254 = 42 -frac_254 = 4.2 -str_254 = "hello" - -# Polymorphic empty collections -empty_list_254 = [] - -# Mixed polymorphic structures -mixed_254 = { - numbers: { value: num_254, list: [num_254, num_254], float: frac }, - strings: { value: str_254, list: [str_254, str_254] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_254 }, - }, - computations: { - from_num: num_254 * 100, - from_frac: frac_254 * 10.0, - list_from_num: [num_254, num_254, num_254], - }, -} - -x_255 = 3.14 -y_255 = 1.23e45 -z_255 = 0.5 - -my_str_255 : Str -my_str_255 = "one" - -binops_255 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_255 : U64 -> U64 -add_one_255 = |n| n + 1 - -map_add_one_255 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_255 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_255 = |arg_one, arg_two| arg_one * arg_two - -num_255 = 42 -frac_255 = 4.2 -str_255 = "hello" - -# Polymorphic empty collections -empty_list_255 = [] - -# Mixed polymorphic structures -mixed_255 = { - numbers: { value: num_255, list: [num_255, num_255], float: frac }, - strings: { value: str_255, list: [str_255, str_255] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_255 }, - }, - computations: { - from_num: num_255 * 100, - from_frac: frac_255 * 10.0, - list_from_num: [num_255, num_255, num_255], - }, -} - -x_256 = 3.14 -y_256 = 1.23e45 -z_256 = 0.5 - -my_str_256 : Str -my_str_256 = "one" - -binops_256 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_256 : U64 -> U64 -add_one_256 = |n| n + 1 - -map_add_one_256 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_256 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_256 = |arg_one, arg_two| arg_one * arg_two - -num_256 = 42 -frac_256 = 4.2 -str_256 = "hello" - -# Polymorphic empty collections -empty_list_256 = [] - -# Mixed polymorphic structures -mixed_256 = { - numbers: { value: num_256, list: [num_256, num_256], float: frac }, - strings: { value: str_256, list: [str_256, str_256] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_256 }, - }, - computations: { - from_num: num_256 * 100, - from_frac: frac_256 * 10.0, - list_from_num: [num_256, num_256, num_256], - }, -} - -x_257 = 3.14 -y_257 = 1.23e45 -z_257 = 0.5 - -my_str_257 : Str -my_str_257 = "one" - -binops_257 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_257 : U64 -> U64 -add_one_257 = |n| n + 1 - -map_add_one_257 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_257 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_257 = |arg_one, arg_two| arg_one * arg_two - -num_257 = 42 -frac_257 = 4.2 -str_257 = "hello" - -# Polymorphic empty collections -empty_list_257 = [] - -# Mixed polymorphic structures -mixed_257 = { - numbers: { value: num_257, list: [num_257, num_257], float: frac }, - strings: { value: str_257, list: [str_257, str_257] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_257 }, - }, - computations: { - from_num: num_257 * 100, - from_frac: frac_257 * 10.0, - list_from_num: [num_257, num_257, num_257], - }, -} - -x_258 = 3.14 -y_258 = 1.23e45 -z_258 = 0.5 - -my_str_258 : Str -my_str_258 = "one" - -binops_258 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_258 : U64 -> U64 -add_one_258 = |n| n + 1 - -map_add_one_258 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_258 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_258 = |arg_one, arg_two| arg_one * arg_two - -num_258 = 42 -frac_258 = 4.2 -str_258 = "hello" - -# Polymorphic empty collections -empty_list_258 = [] - -# Mixed polymorphic structures -mixed_258 = { - numbers: { value: num_258, list: [num_258, num_258], float: frac }, - strings: { value: str_258, list: [str_258, str_258] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_258 }, - }, - computations: { - from_num: num_258 * 100, - from_frac: frac_258 * 10.0, - list_from_num: [num_258, num_258, num_258], - }, -} - -x_259 = 3.14 -y_259 = 1.23e45 -z_259 = 0.5 - -my_str_259 : Str -my_str_259 = "one" - -binops_259 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_259 : U64 -> U64 -add_one_259 = |n| n + 1 - -map_add_one_259 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_259 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_259 = |arg_one, arg_two| arg_one * arg_two - -num_259 = 42 -frac_259 = 4.2 -str_259 = "hello" - -# Polymorphic empty collections -empty_list_259 = [] - -# Mixed polymorphic structures -mixed_259 = { - numbers: { value: num_259, list: [num_259, num_259], float: frac }, - strings: { value: str_259, list: [str_259, str_259] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_259 }, - }, - computations: { - from_num: num_259 * 100, - from_frac: frac_259 * 10.0, - list_from_num: [num_259, num_259, num_259], - }, -} - -x_260 = 3.14 -y_260 = 1.23e45 -z_260 = 0.5 - -my_str_260 : Str -my_str_260 = "one" - -binops_260 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_260 : U64 -> U64 -add_one_260 = |n| n + 1 - -map_add_one_260 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_260 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_260 = |arg_one, arg_two| arg_one * arg_two - -num_260 = 42 -frac_260 = 4.2 -str_260 = "hello" - -# Polymorphic empty collections -empty_list_260 = [] - -# Mixed polymorphic structures -mixed_260 = { - numbers: { value: num_260, list: [num_260, num_260], float: frac }, - strings: { value: str_260, list: [str_260, str_260] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_260 }, - }, - computations: { - from_num: num_260 * 100, - from_frac: frac_260 * 10.0, - list_from_num: [num_260, num_260, num_260], - }, -} - -x_261 = 3.14 -y_261 = 1.23e45 -z_261 = 0.5 - -my_str_261 : Str -my_str_261 = "one" - -binops_261 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_261 : U64 -> U64 -add_one_261 = |n| n + 1 - -map_add_one_261 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_261 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_261 = |arg_one, arg_two| arg_one * arg_two - -num_261 = 42 -frac_261 = 4.2 -str_261 = "hello" - -# Polymorphic empty collections -empty_list_261 = [] - -# Mixed polymorphic structures -mixed_261 = { - numbers: { value: num_261, list: [num_261, num_261], float: frac }, - strings: { value: str_261, list: [str_261, str_261] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_261 }, - }, - computations: { - from_num: num_261 * 100, - from_frac: frac_261 * 10.0, - list_from_num: [num_261, num_261, num_261], - }, -} - -x_262 = 3.14 -y_262 = 1.23e45 -z_262 = 0.5 - -my_str_262 : Str -my_str_262 = "one" - -binops_262 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_262 : U64 -> U64 -add_one_262 = |n| n + 1 - -map_add_one_262 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_262 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_262 = |arg_one, arg_two| arg_one * arg_two - -num_262 = 42 -frac_262 = 4.2 -str_262 = "hello" - -# Polymorphic empty collections -empty_list_262 = [] - -# Mixed polymorphic structures -mixed_262 = { - numbers: { value: num_262, list: [num_262, num_262], float: frac }, - strings: { value: str_262, list: [str_262, str_262] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_262 }, - }, - computations: { - from_num: num_262 * 100, - from_frac: frac_262 * 10.0, - list_from_num: [num_262, num_262, num_262], - }, -} - -x_263 = 3.14 -y_263 = 1.23e45 -z_263 = 0.5 - -my_str_263 : Str -my_str_263 = "one" - -binops_263 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_263 : U64 -> U64 -add_one_263 = |n| n + 1 - -map_add_one_263 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_263 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_263 = |arg_one, arg_two| arg_one * arg_two - -num_263 = 42 -frac_263 = 4.2 -str_263 = "hello" - -# Polymorphic empty collections -empty_list_263 = [] - -# Mixed polymorphic structures -mixed_263 = { - numbers: { value: num_263, list: [num_263, num_263], float: frac }, - strings: { value: str_263, list: [str_263, str_263] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_263 }, - }, - computations: { - from_num: num_263 * 100, - from_frac: frac_263 * 10.0, - list_from_num: [num_263, num_263, num_263], - }, -} - -x_264 = 3.14 -y_264 = 1.23e45 -z_264 = 0.5 - -my_str_264 : Str -my_str_264 = "one" - -binops_264 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_264 : U64 -> U64 -add_one_264 = |n| n + 1 - -map_add_one_264 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_264 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_264 = |arg_one, arg_two| arg_one * arg_two - -num_264 = 42 -frac_264 = 4.2 -str_264 = "hello" - -# Polymorphic empty collections -empty_list_264 = [] - -# Mixed polymorphic structures -mixed_264 = { - numbers: { value: num_264, list: [num_264, num_264], float: frac }, - strings: { value: str_264, list: [str_264, str_264] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_264 }, - }, - computations: { - from_num: num_264 * 100, - from_frac: frac_264 * 10.0, - list_from_num: [num_264, num_264, num_264], - }, -} - -x_265 = 3.14 -y_265 = 1.23e45 -z_265 = 0.5 - -my_str_265 : Str -my_str_265 = "one" - -binops_265 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_265 : U64 -> U64 -add_one_265 = |n| n + 1 - -map_add_one_265 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_265 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_265 = |arg_one, arg_two| arg_one * arg_two - -num_265 = 42 -frac_265 = 4.2 -str_265 = "hello" - -# Polymorphic empty collections -empty_list_265 = [] - -# Mixed polymorphic structures -mixed_265 = { - numbers: { value: num_265, list: [num_265, num_265], float: frac }, - strings: { value: str_265, list: [str_265, str_265] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_265 }, - }, - computations: { - from_num: num_265 * 100, - from_frac: frac_265 * 10.0, - list_from_num: [num_265, num_265, num_265], - }, -} - -x_266 = 3.14 -y_266 = 1.23e45 -z_266 = 0.5 - -my_str_266 : Str -my_str_266 = "one" - -binops_266 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_266 : U64 -> U64 -add_one_266 = |n| n + 1 - -map_add_one_266 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_266 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_266 = |arg_one, arg_two| arg_one * arg_two - -num_266 = 42 -frac_266 = 4.2 -str_266 = "hello" - -# Polymorphic empty collections -empty_list_266 = [] - -# Mixed polymorphic structures -mixed_266 = { - numbers: { value: num_266, list: [num_266, num_266], float: frac }, - strings: { value: str_266, list: [str_266, str_266] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_266 }, - }, - computations: { - from_num: num_266 * 100, - from_frac: frac_266 * 10.0, - list_from_num: [num_266, num_266, num_266], - }, -} - -x_267 = 3.14 -y_267 = 1.23e45 -z_267 = 0.5 - -my_str_267 : Str -my_str_267 = "one" - -binops_267 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_267 : U64 -> U64 -add_one_267 = |n| n + 1 - -map_add_one_267 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_267 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_267 = |arg_one, arg_two| arg_one * arg_two - -num_267 = 42 -frac_267 = 4.2 -str_267 = "hello" - -# Polymorphic empty collections -empty_list_267 = [] - -# Mixed polymorphic structures -mixed_267 = { - numbers: { value: num_267, list: [num_267, num_267], float: frac }, - strings: { value: str_267, list: [str_267, str_267] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_267 }, - }, - computations: { - from_num: num_267 * 100, - from_frac: frac_267 * 10.0, - list_from_num: [num_267, num_267, num_267], - }, -} - -x_268 = 3.14 -y_268 = 1.23e45 -z_268 = 0.5 - -my_str_268 : Str -my_str_268 = "one" - -binops_268 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_268 : U64 -> U64 -add_one_268 = |n| n + 1 - -map_add_one_268 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_268 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_268 = |arg_one, arg_two| arg_one * arg_two - -num_268 = 42 -frac_268 = 4.2 -str_268 = "hello" - -# Polymorphic empty collections -empty_list_268 = [] - -# Mixed polymorphic structures -mixed_268 = { - numbers: { value: num_268, list: [num_268, num_268], float: frac }, - strings: { value: str_268, list: [str_268, str_268] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_268 }, - }, - computations: { - from_num: num_268 * 100, - from_frac: frac_268 * 10.0, - list_from_num: [num_268, num_268, num_268], - }, -} - -x_269 = 3.14 -y_269 = 1.23e45 -z_269 = 0.5 - -my_str_269 : Str -my_str_269 = "one" - -binops_269 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_269 : U64 -> U64 -add_one_269 = |n| n + 1 - -map_add_one_269 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_269 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_269 = |arg_one, arg_two| arg_one * arg_two - -num_269 = 42 -frac_269 = 4.2 -str_269 = "hello" - -# Polymorphic empty collections -empty_list_269 = [] - -# Mixed polymorphic structures -mixed_269 = { - numbers: { value: num_269, list: [num_269, num_269], float: frac }, - strings: { value: str_269, list: [str_269, str_269] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_269 }, - }, - computations: { - from_num: num_269 * 100, - from_frac: frac_269 * 10.0, - list_from_num: [num_269, num_269, num_269], - }, -} - -x_270 = 3.14 -y_270 = 1.23e45 -z_270 = 0.5 - -my_str_270 : Str -my_str_270 = "one" - -binops_270 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_270 : U64 -> U64 -add_one_270 = |n| n + 1 - -map_add_one_270 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_270 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_270 = |arg_one, arg_two| arg_one * arg_two - -num_270 = 42 -frac_270 = 4.2 -str_270 = "hello" - -# Polymorphic empty collections -empty_list_270 = [] - -# Mixed polymorphic structures -mixed_270 = { - numbers: { value: num_270, list: [num_270, num_270], float: frac }, - strings: { value: str_270, list: [str_270, str_270] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_270 }, - }, - computations: { - from_num: num_270 * 100, - from_frac: frac_270 * 10.0, - list_from_num: [num_270, num_270, num_270], - }, -} - -x_271 = 3.14 -y_271 = 1.23e45 -z_271 = 0.5 - -my_str_271 : Str -my_str_271 = "one" - -binops_271 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_271 : U64 -> U64 -add_one_271 = |n| n + 1 - -map_add_one_271 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_271 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_271 = |arg_one, arg_two| arg_one * arg_two - -num_271 = 42 -frac_271 = 4.2 -str_271 = "hello" - -# Polymorphic empty collections -empty_list_271 = [] - -# Mixed polymorphic structures -mixed_271 = { - numbers: { value: num_271, list: [num_271, num_271], float: frac }, - strings: { value: str_271, list: [str_271, str_271] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_271 }, - }, - computations: { - from_num: num_271 * 100, - from_frac: frac_271 * 10.0, - list_from_num: [num_271, num_271, num_271], - }, -} - -x_272 = 3.14 -y_272 = 1.23e45 -z_272 = 0.5 - -my_str_272 : Str -my_str_272 = "one" - -binops_272 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_272 : U64 -> U64 -add_one_272 = |n| n + 1 - -map_add_one_272 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_272 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_272 = |arg_one, arg_two| arg_one * arg_two - -num_272 = 42 -frac_272 = 4.2 -str_272 = "hello" - -# Polymorphic empty collections -empty_list_272 = [] - -# Mixed polymorphic structures -mixed_272 = { - numbers: { value: num_272, list: [num_272, num_272], float: frac }, - strings: { value: str_272, list: [str_272, str_272] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_272 }, - }, - computations: { - from_num: num_272 * 100, - from_frac: frac_272 * 10.0, - list_from_num: [num_272, num_272, num_272], - }, -} - -x_273 = 3.14 -y_273 = 1.23e45 -z_273 = 0.5 - -my_str_273 : Str -my_str_273 = "one" - -binops_273 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_273 : U64 -> U64 -add_one_273 = |n| n + 1 - -map_add_one_273 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_273 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_273 = |arg_one, arg_two| arg_one * arg_two - -num_273 = 42 -frac_273 = 4.2 -str_273 = "hello" - -# Polymorphic empty collections -empty_list_273 = [] - -# Mixed polymorphic structures -mixed_273 = { - numbers: { value: num_273, list: [num_273, num_273], float: frac }, - strings: { value: str_273, list: [str_273, str_273] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_273 }, - }, - computations: { - from_num: num_273 * 100, - from_frac: frac_273 * 10.0, - list_from_num: [num_273, num_273, num_273], - }, -} - -x_274 = 3.14 -y_274 = 1.23e45 -z_274 = 0.5 - -my_str_274 : Str -my_str_274 = "one" - -binops_274 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_274 : U64 -> U64 -add_one_274 = |n| n + 1 - -map_add_one_274 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_274 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_274 = |arg_one, arg_two| arg_one * arg_two - -num_274 = 42 -frac_274 = 4.2 -str_274 = "hello" - -# Polymorphic empty collections -empty_list_274 = [] - -# Mixed polymorphic structures -mixed_274 = { - numbers: { value: num_274, list: [num_274, num_274], float: frac }, - strings: { value: str_274, list: [str_274, str_274] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_274 }, - }, - computations: { - from_num: num_274 * 100, - from_frac: frac_274 * 10.0, - list_from_num: [num_274, num_274, num_274], - }, -} - -x_275 = 3.14 -y_275 = 1.23e45 -z_275 = 0.5 - -my_str_275 : Str -my_str_275 = "one" - -binops_275 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_275 : U64 -> U64 -add_one_275 = |n| n + 1 - -map_add_one_275 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_275 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_275 = |arg_one, arg_two| arg_one * arg_two - -num_275 = 42 -frac_275 = 4.2 -str_275 = "hello" - -# Polymorphic empty collections -empty_list_275 = [] - -# Mixed polymorphic structures -mixed_275 = { - numbers: { value: num_275, list: [num_275, num_275], float: frac }, - strings: { value: str_275, list: [str_275, str_275] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_275 }, - }, - computations: { - from_num: num_275 * 100, - from_frac: frac_275 * 10.0, - list_from_num: [num_275, num_275, num_275], - }, -} - -x_276 = 3.14 -y_276 = 1.23e45 -z_276 = 0.5 - -my_str_276 : Str -my_str_276 = "one" - -binops_276 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_276 : U64 -> U64 -add_one_276 = |n| n + 1 - -map_add_one_276 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_276 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_276 = |arg_one, arg_two| arg_one * arg_two - -num_276 = 42 -frac_276 = 4.2 -str_276 = "hello" - -# Polymorphic empty collections -empty_list_276 = [] - -# Mixed polymorphic structures -mixed_276 = { - numbers: { value: num_276, list: [num_276, num_276], float: frac }, - strings: { value: str_276, list: [str_276, str_276] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_276 }, - }, - computations: { - from_num: num_276 * 100, - from_frac: frac_276 * 10.0, - list_from_num: [num_276, num_276, num_276], - }, -} - -x_277 = 3.14 -y_277 = 1.23e45 -z_277 = 0.5 - -my_str_277 : Str -my_str_277 = "one" - -binops_277 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_277 : U64 -> U64 -add_one_277 = |n| n + 1 - -map_add_one_277 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_277 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_277 = |arg_one, arg_two| arg_one * arg_two - -num_277 = 42 -frac_277 = 4.2 -str_277 = "hello" - -# Polymorphic empty collections -empty_list_277 = [] - -# Mixed polymorphic structures -mixed_277 = { - numbers: { value: num_277, list: [num_277, num_277], float: frac }, - strings: { value: str_277, list: [str_277, str_277] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_277 }, - }, - computations: { - from_num: num_277 * 100, - from_frac: frac_277 * 10.0, - list_from_num: [num_277, num_277, num_277], - }, -} - -x_278 = 3.14 -y_278 = 1.23e45 -z_278 = 0.5 - -my_str_278 : Str -my_str_278 = "one" - -binops_278 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_278 : U64 -> U64 -add_one_278 = |n| n + 1 - -map_add_one_278 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_278 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_278 = |arg_one, arg_two| arg_one * arg_two - -num_278 = 42 -frac_278 = 4.2 -str_278 = "hello" - -# Polymorphic empty collections -empty_list_278 = [] - -# Mixed polymorphic structures -mixed_278 = { - numbers: { value: num_278, list: [num_278, num_278], float: frac }, - strings: { value: str_278, list: [str_278, str_278] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_278 }, - }, - computations: { - from_num: num_278 * 100, - from_frac: frac_278 * 10.0, - list_from_num: [num_278, num_278, num_278], - }, -} - -x_279 = 3.14 -y_279 = 1.23e45 -z_279 = 0.5 - -my_str_279 : Str -my_str_279 = "one" - -binops_279 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_279 : U64 -> U64 -add_one_279 = |n| n + 1 - -map_add_one_279 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_279 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_279 = |arg_one, arg_two| arg_one * arg_two - -num_279 = 42 -frac_279 = 4.2 -str_279 = "hello" - -# Polymorphic empty collections -empty_list_279 = [] - -# Mixed polymorphic structures -mixed_279 = { - numbers: { value: num_279, list: [num_279, num_279], float: frac }, - strings: { value: str_279, list: [str_279, str_279] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_279 }, - }, - computations: { - from_num: num_279 * 100, - from_frac: frac_279 * 10.0, - list_from_num: [num_279, num_279, num_279], - }, -} - -x_280 = 3.14 -y_280 = 1.23e45 -z_280 = 0.5 - -my_str_280 : Str -my_str_280 = "one" - -binops_280 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_280 : U64 -> U64 -add_one_280 = |n| n + 1 - -map_add_one_280 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_280 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_280 = |arg_one, arg_two| arg_one * arg_two - -num_280 = 42 -frac_280 = 4.2 -str_280 = "hello" - -# Polymorphic empty collections -empty_list_280 = [] - -# Mixed polymorphic structures -mixed_280 = { - numbers: { value: num_280, list: [num_280, num_280], float: frac }, - strings: { value: str_280, list: [str_280, str_280] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_280 }, - }, - computations: { - from_num: num_280 * 100, - from_frac: frac_280 * 10.0, - list_from_num: [num_280, num_280, num_280], - }, -} - -x_281 = 3.14 -y_281 = 1.23e45 -z_281 = 0.5 - -my_str_281 : Str -my_str_281 = "one" - -binops_281 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_281 : U64 -> U64 -add_one_281 = |n| n + 1 - -map_add_one_281 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_281 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_281 = |arg_one, arg_two| arg_one * arg_two - -num_281 = 42 -frac_281 = 4.2 -str_281 = "hello" - -# Polymorphic empty collections -empty_list_281 = [] - -# Mixed polymorphic structures -mixed_281 = { - numbers: { value: num_281, list: [num_281, num_281], float: frac }, - strings: { value: str_281, list: [str_281, str_281] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_281 }, - }, - computations: { - from_num: num_281 * 100, - from_frac: frac_281 * 10.0, - list_from_num: [num_281, num_281, num_281], - }, -} - -x_282 = 3.14 -y_282 = 1.23e45 -z_282 = 0.5 - -my_str_282 : Str -my_str_282 = "one" - -binops_282 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_282 : U64 -> U64 -add_one_282 = |n| n + 1 - -map_add_one_282 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_282 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_282 = |arg_one, arg_two| arg_one * arg_two - -num_282 = 42 -frac_282 = 4.2 -str_282 = "hello" - -# Polymorphic empty collections -empty_list_282 = [] - -# Mixed polymorphic structures -mixed_282 = { - numbers: { value: num_282, list: [num_282, num_282], float: frac }, - strings: { value: str_282, list: [str_282, str_282] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_282 }, - }, - computations: { - from_num: num_282 * 100, - from_frac: frac_282 * 10.0, - list_from_num: [num_282, num_282, num_282], - }, -} - -x_283 = 3.14 -y_283 = 1.23e45 -z_283 = 0.5 - -my_str_283 : Str -my_str_283 = "one" - -binops_283 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_283 : U64 -> U64 -add_one_283 = |n| n + 1 - -map_add_one_283 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_283 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_283 = |arg_one, arg_two| arg_one * arg_two - -num_283 = 42 -frac_283 = 4.2 -str_283 = "hello" - -# Polymorphic empty collections -empty_list_283 = [] - -# Mixed polymorphic structures -mixed_283 = { - numbers: { value: num_283, list: [num_283, num_283], float: frac }, - strings: { value: str_283, list: [str_283, str_283] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_283 }, - }, - computations: { - from_num: num_283 * 100, - from_frac: frac_283 * 10.0, - list_from_num: [num_283, num_283, num_283], - }, -} - -x_284 = 3.14 -y_284 = 1.23e45 -z_284 = 0.5 - -my_str_284 : Str -my_str_284 = "one" - -binops_284 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_284 : U64 -> U64 -add_one_284 = |n| n + 1 - -map_add_one_284 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_284 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_284 = |arg_one, arg_two| arg_one * arg_two - -num_284 = 42 -frac_284 = 4.2 -str_284 = "hello" - -# Polymorphic empty collections -empty_list_284 = [] - -# Mixed polymorphic structures -mixed_284 = { - numbers: { value: num_284, list: [num_284, num_284], float: frac }, - strings: { value: str_284, list: [str_284, str_284] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_284 }, - }, - computations: { - from_num: num_284 * 100, - from_frac: frac_284 * 10.0, - list_from_num: [num_284, num_284, num_284], - }, -} - -x_285 = 3.14 -y_285 = 1.23e45 -z_285 = 0.5 - -my_str_285 : Str -my_str_285 = "one" - -binops_285 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_285 : U64 -> U64 -add_one_285 = |n| n + 1 - -map_add_one_285 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_285 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_285 = |arg_one, arg_two| arg_one * arg_two - -num_285 = 42 -frac_285 = 4.2 -str_285 = "hello" - -# Polymorphic empty collections -empty_list_285 = [] - -# Mixed polymorphic structures -mixed_285 = { - numbers: { value: num_285, list: [num_285, num_285], float: frac }, - strings: { value: str_285, list: [str_285, str_285] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_285 }, - }, - computations: { - from_num: num_285 * 100, - from_frac: frac_285 * 10.0, - list_from_num: [num_285, num_285, num_285], - }, -} - -x_286 = 3.14 -y_286 = 1.23e45 -z_286 = 0.5 - -my_str_286 : Str -my_str_286 = "one" - -binops_286 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_286 : U64 -> U64 -add_one_286 = |n| n + 1 - -map_add_one_286 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_286 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_286 = |arg_one, arg_two| arg_one * arg_two - -num_286 = 42 -frac_286 = 4.2 -str_286 = "hello" - -# Polymorphic empty collections -empty_list_286 = [] - -# Mixed polymorphic structures -mixed_286 = { - numbers: { value: num_286, list: [num_286, num_286], float: frac }, - strings: { value: str_286, list: [str_286, str_286] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_286 }, - }, - computations: { - from_num: num_286 * 100, - from_frac: frac_286 * 10.0, - list_from_num: [num_286, num_286, num_286], - }, -} - -x_287 = 3.14 -y_287 = 1.23e45 -z_287 = 0.5 - -my_str_287 : Str -my_str_287 = "one" - -binops_287 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_287 : U64 -> U64 -add_one_287 = |n| n + 1 - -map_add_one_287 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_287 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_287 = |arg_one, arg_two| arg_one * arg_two - -num_287 = 42 -frac_287 = 4.2 -str_287 = "hello" - -# Polymorphic empty collections -empty_list_287 = [] - -# Mixed polymorphic structures -mixed_287 = { - numbers: { value: num_287, list: [num_287, num_287], float: frac }, - strings: { value: str_287, list: [str_287, str_287] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_287 }, - }, - computations: { - from_num: num_287 * 100, - from_frac: frac_287 * 10.0, - list_from_num: [num_287, num_287, num_287], - }, -} - -x_288 = 3.14 -y_288 = 1.23e45 -z_288 = 0.5 - -my_str_288 : Str -my_str_288 = "one" - -binops_288 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_288 : U64 -> U64 -add_one_288 = |n| n + 1 - -map_add_one_288 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_288 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_288 = |arg_one, arg_two| arg_one * arg_two - -num_288 = 42 -frac_288 = 4.2 -str_288 = "hello" - -# Polymorphic empty collections -empty_list_288 = [] - -# Mixed polymorphic structures -mixed_288 = { - numbers: { value: num_288, list: [num_288, num_288], float: frac }, - strings: { value: str_288, list: [str_288, str_288] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_288 }, - }, - computations: { - from_num: num_288 * 100, - from_frac: frac_288 * 10.0, - list_from_num: [num_288, num_288, num_288], - }, -} - -x_289 = 3.14 -y_289 = 1.23e45 -z_289 = 0.5 - -my_str_289 : Str -my_str_289 = "one" - -binops_289 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_289 : U64 -> U64 -add_one_289 = |n| n + 1 - -map_add_one_289 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_289 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_289 = |arg_one, arg_two| arg_one * arg_two - -num_289 = 42 -frac_289 = 4.2 -str_289 = "hello" - -# Polymorphic empty collections -empty_list_289 = [] - -# Mixed polymorphic structures -mixed_289 = { - numbers: { value: num_289, list: [num_289, num_289], float: frac }, - strings: { value: str_289, list: [str_289, str_289] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_289 }, - }, - computations: { - from_num: num_289 * 100, - from_frac: frac_289 * 10.0, - list_from_num: [num_289, num_289, num_289], - }, -} - -x_290 = 3.14 -y_290 = 1.23e45 -z_290 = 0.5 - -my_str_290 : Str -my_str_290 = "one" - -binops_290 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_290 : U64 -> U64 -add_one_290 = |n| n + 1 - -map_add_one_290 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_290 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_290 = |arg_one, arg_two| arg_one * arg_two - -num_290 = 42 -frac_290 = 4.2 -str_290 = "hello" - -# Polymorphic empty collections -empty_list_290 = [] - -# Mixed polymorphic structures -mixed_290 = { - numbers: { value: num_290, list: [num_290, num_290], float: frac }, - strings: { value: str_290, list: [str_290, str_290] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_290 }, - }, - computations: { - from_num: num_290 * 100, - from_frac: frac_290 * 10.0, - list_from_num: [num_290, num_290, num_290], - }, -} - -x_291 = 3.14 -y_291 = 1.23e45 -z_291 = 0.5 - -my_str_291 : Str -my_str_291 = "one" - -binops_291 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_291 : U64 -> U64 -add_one_291 = |n| n + 1 - -map_add_one_291 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_291 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_291 = |arg_one, arg_two| arg_one * arg_two - -num_291 = 42 -frac_291 = 4.2 -str_291 = "hello" - -# Polymorphic empty collections -empty_list_291 = [] - -# Mixed polymorphic structures -mixed_291 = { - numbers: { value: num_291, list: [num_291, num_291], float: frac }, - strings: { value: str_291, list: [str_291, str_291] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_291 }, - }, - computations: { - from_num: num_291 * 100, - from_frac: frac_291 * 10.0, - list_from_num: [num_291, num_291, num_291], - }, -} - -x_292 = 3.14 -y_292 = 1.23e45 -z_292 = 0.5 - -my_str_292 : Str -my_str_292 = "one" - -binops_292 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_292 : U64 -> U64 -add_one_292 = |n| n + 1 - -map_add_one_292 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_292 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_292 = |arg_one, arg_two| arg_one * arg_two - -num_292 = 42 -frac_292 = 4.2 -str_292 = "hello" - -# Polymorphic empty collections -empty_list_292 = [] - -# Mixed polymorphic structures -mixed_292 = { - numbers: { value: num_292, list: [num_292, num_292], float: frac }, - strings: { value: str_292, list: [str_292, str_292] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_292 }, - }, - computations: { - from_num: num_292 * 100, - from_frac: frac_292 * 10.0, - list_from_num: [num_292, num_292, num_292], - }, -} - -x_293 = 3.14 -y_293 = 1.23e45 -z_293 = 0.5 - -my_str_293 : Str -my_str_293 = "one" - -binops_293 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_293 : U64 -> U64 -add_one_293 = |n| n + 1 - -map_add_one_293 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_293 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_293 = |arg_one, arg_two| arg_one * arg_two - -num_293 = 42 -frac_293 = 4.2 -str_293 = "hello" - -# Polymorphic empty collections -empty_list_293 = [] - -# Mixed polymorphic structures -mixed_293 = { - numbers: { value: num_293, list: [num_293, num_293], float: frac }, - strings: { value: str_293, list: [str_293, str_293] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_293 }, - }, - computations: { - from_num: num_293 * 100, - from_frac: frac_293 * 10.0, - list_from_num: [num_293, num_293, num_293], - }, -} - -x_294 = 3.14 -y_294 = 1.23e45 -z_294 = 0.5 - -my_str_294 : Str -my_str_294 = "one" - -binops_294 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_294 : U64 -> U64 -add_one_294 = |n| n + 1 - -map_add_one_294 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_294 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_294 = |arg_one, arg_two| arg_one * arg_two - -num_294 = 42 -frac_294 = 4.2 -str_294 = "hello" - -# Polymorphic empty collections -empty_list_294 = [] - -# Mixed polymorphic structures -mixed_294 = { - numbers: { value: num_294, list: [num_294, num_294], float: frac }, - strings: { value: str_294, list: [str_294, str_294] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_294 }, - }, - computations: { - from_num: num_294 * 100, - from_frac: frac_294 * 10.0, - list_from_num: [num_294, num_294, num_294], - }, -} - -x_295 = 3.14 -y_295 = 1.23e45 -z_295 = 0.5 - -my_str_295 : Str -my_str_295 = "one" - -binops_295 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_295 : U64 -> U64 -add_one_295 = |n| n + 1 - -map_add_one_295 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_295 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_295 = |arg_one, arg_two| arg_one * arg_two - -num_295 = 42 -frac_295 = 4.2 -str_295 = "hello" - -# Polymorphic empty collections -empty_list_295 = [] - -# Mixed polymorphic structures -mixed_295 = { - numbers: { value: num_295, list: [num_295, num_295], float: frac }, - strings: { value: str_295, list: [str_295, str_295] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_295 }, - }, - computations: { - from_num: num_295 * 100, - from_frac: frac_295 * 10.0, - list_from_num: [num_295, num_295, num_295], - }, -} - -x_296 = 3.14 -y_296 = 1.23e45 -z_296 = 0.5 - -my_str_296 : Str -my_str_296 = "one" - -binops_296 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_296 : U64 -> U64 -add_one_296 = |n| n + 1 - -map_add_one_296 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_296 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_296 = |arg_one, arg_two| arg_one * arg_two - -num_296 = 42 -frac_296 = 4.2 -str_296 = "hello" - -# Polymorphic empty collections -empty_list_296 = [] - -# Mixed polymorphic structures -mixed_296 = { - numbers: { value: num_296, list: [num_296, num_296], float: frac }, - strings: { value: str_296, list: [str_296, str_296] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_296 }, - }, - computations: { - from_num: num_296 * 100, - from_frac: frac_296 * 10.0, - list_from_num: [num_296, num_296, num_296], - }, -} - -x_297 = 3.14 -y_297 = 1.23e45 -z_297 = 0.5 - -my_str_297 : Str -my_str_297 = "one" - -binops_297 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_297 : U64 -> U64 -add_one_297 = |n| n + 1 - -map_add_one_297 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_297 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_297 = |arg_one, arg_two| arg_one * arg_two - -num_297 = 42 -frac_297 = 4.2 -str_297 = "hello" - -# Polymorphic empty collections -empty_list_297 = [] - -# Mixed polymorphic structures -mixed_297 = { - numbers: { value: num_297, list: [num_297, num_297], float: frac }, - strings: { value: str_297, list: [str_297, str_297] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_297 }, - }, - computations: { - from_num: num_297 * 100, - from_frac: frac_297 * 10.0, - list_from_num: [num_297, num_297, num_297], - }, -} - -x_298 = 3.14 -y_298 = 1.23e45 -z_298 = 0.5 - -my_str_298 : Str -my_str_298 = "one" - -binops_298 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_298 : U64 -> U64 -add_one_298 = |n| n + 1 - -map_add_one_298 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_298 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_298 = |arg_one, arg_two| arg_one * arg_two - -num_298 = 42 -frac_298 = 4.2 -str_298 = "hello" - -# Polymorphic empty collections -empty_list_298 = [] - -# Mixed polymorphic structures -mixed_298 = { - numbers: { value: num_298, list: [num_298, num_298], float: frac }, - strings: { value: str_298, list: [str_298, str_298] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_298 }, - }, - computations: { - from_num: num_298 * 100, - from_frac: frac_298 * 10.0, - list_from_num: [num_298, num_298, num_298], - }, -} - -x_299 = 3.14 -y_299 = 1.23e45 -z_299 = 0.5 - -my_str_299 : Str -my_str_299 = "one" - -binops_299 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_299 : U64 -> U64 -add_one_299 = |n| n + 1 - -map_add_one_299 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_299 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_299 = |arg_one, arg_two| arg_one * arg_two - -num_299 = 42 -frac_299 = 4.2 -str_299 = "hello" - -# Polymorphic empty collections -empty_list_299 = [] - -# Mixed polymorphic structures -mixed_299 = { - numbers: { value: num_299, list: [num_299, num_299], float: frac }, - strings: { value: str_299, list: [str_299, str_299] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_299 }, - }, - computations: { - from_num: num_299 * 100, - from_frac: frac_299 * 10.0, - list_from_num: [num_299, num_299, num_299], - }, -} - -x_300 = 3.14 -y_300 = 1.23e45 -z_300 = 0.5 - -my_str_300 : Str -my_str_300 = "one" - -binops_300 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_300 : U64 -> U64 -add_one_300 = |n| n + 1 - -map_add_one_300 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_300 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_300 = |arg_one, arg_two| arg_one * arg_two - -num_300 = 42 -frac_300 = 4.2 -str_300 = "hello" - -# Polymorphic empty collections -empty_list_300 = [] - -# Mixed polymorphic structures -mixed_300 = { - numbers: { value: num_300, list: [num_300, num_300], float: frac }, - strings: { value: str_300, list: [str_300, str_300] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_300 }, - }, - computations: { - from_num: num_300 * 100, - from_frac: frac_300 * 10.0, - list_from_num: [num_300, num_300, num_300], - }, -} - -x_301 = 3.14 -y_301 = 1.23e45 -z_301 = 0.5 - -my_str_301 : Str -my_str_301 = "one" - -binops_301 = ( - 4 + 2, - 4 - 2, - 4 * 2, - 4 / 2, - 4 % 2, - 4 < 2, - 4 > 2, - 4 <= 2, - 4 >= 2, - 4 == 2, - 4 != 2, - 4 // 2, -) - -add_one_301 : U64 -> U64 -add_one_301 = |n| n + 1 - -map_add_one_301 = |list| { - fn = |numy| numy + 1 - list.map(fn) -} - -# Function showing var vs regular identifier independence -test_func_301 = |input| { - sum = input # Regular identifier - var sum_ = input * 2 # Var with underscore - should not conflict - - sum_ = sum_ + sum # Reassign var - should work - sum + sum_ # Both should be accessible -} - -multiply_301 = |arg_one, arg_two| arg_one * arg_two - -num_301 = 42 -frac_301 = 4.2 -str_301 = "hello" - -# Polymorphic empty collections -empty_list_301 = [] - -# Mixed polymorphic structures -mixed_301 = { - numbers: { value: num_301, list: [num_301, num_301], float: frac }, - strings: { value: str_301, list: [str_301, str_301] }, - empty_lists: { - raw: empty_list, - in_list: [empty_list], - in_record: { data: empty_list_301 }, - }, - computations: { - from_num: num_301 * 100, - from_frac: frac_301 * 10.0, - list_from_num: [num_301, num_301, num_301], - }, -} +app [main] { pf: platform "../../test/str/platform/main.roc" } +main = |_| 1 + 1 diff --git a/src/PROFILING/bench_repeated_check_ORIGINAL.roc b/src/PROFILING/bench_repeated_check_ORIGINAL.roc new file mode 100644 index 0000000000..f51a1a7379 --- /dev/null +++ b/src/PROFILING/bench_repeated_check_ORIGINAL.roc @@ -0,0 +1,19269 @@ +## +## !! Do not alter this file unless necessary !! +## +## Compiler phase benchmarks use this file, see `src/PROFILING/exec_bench.roc`. +## If the file changes, the benchmarks can't track performance over time. + +x = 3.14 +y = 1.23e45 +z = 0.5 + +my_str : Str +my_str = "one" + +binops = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one : U64 -> U64 +add_one = |n| n + 1 + +map_add_one = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply = |arg_one, arg_two| arg_one * arg_two + +num = 42 +frac = 4.2 +str = "hello" + +# Polymorphic empty collections +empty_list = [] + +# Mixed polymorphic structures +mixed = { + numbers: { value: num, list: [num, num], float: frac }, + strings: { value: str, list: [str, str] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list }, + }, + computations: { + from_num: num * 100, + from_frac: frac * 10.0, + list_from_num: [num, num, num], + }, +} + +x_2 = 3.14 +y_2 = 1.23e45 +z_2 = 0.5 + +my_str_2 : Str +my_str_2 = "one" + +binops_2 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_2 : U64 -> U64 +add_one_2 = |n| n + 1 + +map_add_one_2 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_2 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_2 = |arg_one, arg_two| arg_one * arg_two + +num_2 = 42 +frac_2 = 4.2 +str_2 = "hello" + +# Polymorphic empty collections +empty_list_2 = [] + +# Mixed polymorphic structures +mixed_2 = { + numbers: { value: num_2, list: [num_2, num_2], float: frac }, + strings: { value: str_2, list: [str_2, str_2] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_2 }, + }, + computations: { + from_num: num_2 * 100, + from_frac: frac_2 * 10.0, + list_from_num: [num_2, num_2, num_2], + }, +} + +x_3 = 3.14 +y_3 = 1.23e45 +z_3 = 0.5 + +my_str_3 : Str +my_str_3 = "one" + +binops_3 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_3 : U64 -> U64 +add_one_3 = |n| n + 1 + +map_add_one_3 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_3 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_3 = |arg_one, arg_two| arg_one * arg_two + +num_3 = 42 +frac_3 = 4.2 +str_3 = "hello" + +# Polymorphic empty collections +empty_list_3 = [] + +# Mixed polymorphic structures +mixed_3 = { + numbers: { value: num_3, list: [num_3, num_3], float: frac }, + strings: { value: str_3, list: [str_3, str_3] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_3 }, + }, + computations: { + from_num: num_3 * 100, + from_frac: frac_3 * 10.0, + list_from_num: [num_3, num_3, num_3], + }, +} + +x_4 = 3.14 +y_4 = 1.23e45 +z_4 = 0.5 + +my_str_4 : Str +my_str_4 = "one" + +binops_4 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_4 : U64 -> U64 +add_one_4 = |n| n + 1 + +map_add_one_4 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_4 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_4 = |arg_one, arg_two| arg_one * arg_two + +num_4 = 42 +frac_4 = 4.2 +str_4 = "hello" + +# Polymorphic empty collections +empty_list_4 = [] + +# Mixed polymorphic structures +mixed_4 = { + numbers: { value: num_4, list: [num_4, num_4], float: frac }, + strings: { value: str_4, list: [str_4, str_4] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_4 }, + }, + computations: { + from_num: num_4 * 100, + from_frac: frac_4 * 10.0, + list_from_num: [num_4, num_4, num_4], + }, +} + +x_5 = 3.14 +y_5 = 1.23e45 +z_5 = 0.5 + +my_str_5 : Str +my_str_5 = "one" + +binops_5 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_5 : U64 -> U64 +add_one_5 = |n| n + 1 + +map_add_one_5 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_5 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_5 = |arg_one, arg_two| arg_one * arg_two + +num_5 = 42 +frac_5 = 4.2 +str_5 = "hello" + +# Polymorphic empty collections +empty_list_5 = [] + +# Mixed polymorphic structures +mixed_5 = { + numbers: { value: num_5, list: [num_5, num_5], float: frac }, + strings: { value: str_5, list: [str_5, str_5] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_5 }, + }, + computations: { + from_num: num_5 * 100, + from_frac: frac_5 * 10.0, + list_from_num: [num_5, num_5, num_5], + }, +} + +x_6 = 3.14 +y_6 = 1.23e45 +z_6 = 0.5 + +my_str_6 : Str +my_str_6 = "one" + +binops_6 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_6 : U64 -> U64 +add_one_6 = |n| n + 1 + +map_add_one_6 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_6 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_6 = |arg_one, arg_two| arg_one * arg_two + +num_6 = 42 +frac_6 = 4.2 +str_6 = "hello" + +# Polymorphic empty collections +empty_list_6 = [] + +# Mixed polymorphic structures +mixed_6 = { + numbers: { value: num_6, list: [num_6, num_6], float: frac }, + strings: { value: str_6, list: [str_6, str_6] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_6 }, + }, + computations: { + from_num: num_6 * 100, + from_frac: frac_6 * 10.0, + list_from_num: [num_6, num_6, num_6], + }, +} + +x_7 = 3.14 +y_7 = 1.23e45 +z_7 = 0.5 + +my_str_7 : Str +my_str_7 = "one" + +binops_7 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_7 : U64 -> U64 +add_one_7 = |n| n + 1 + +map_add_one_7 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_7 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_7 = |arg_one, arg_two| arg_one * arg_two + +num_7 = 42 +frac_7 = 4.2 +str_7 = "hello" + +# Polymorphic empty collections +empty_list_7 = [] + +# Mixed polymorphic structures +mixed_7 = { + numbers: { value: num_7, list: [num_7, num_7], float: frac }, + strings: { value: str_7, list: [str_7, str_7] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_7 }, + }, + computations: { + from_num: num_7 * 100, + from_frac: frac_7 * 10.0, + list_from_num: [num_7, num_7, num_7], + }, +} + +x_8 = 3.14 +y_8 = 1.23e45 +z_8 = 0.5 + +my_str_8 : Str +my_str_8 = "one" + +binops_8 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_8 : U64 -> U64 +add_one_8 = |n| n + 1 + +map_add_one_8 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_8 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_8 = |arg_one, arg_two| arg_one * arg_two + +num_8 = 42 +frac_8 = 4.2 +str_8 = "hello" + +# Polymorphic empty collections +empty_list_8 = [] + +# Mixed polymorphic structures +mixed_8 = { + numbers: { value: num_8, list: [num_8, num_8], float: frac }, + strings: { value: str_8, list: [str_8, str_8] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_8 }, + }, + computations: { + from_num: num_8 * 100, + from_frac: frac_8 * 10.0, + list_from_num: [num_8, num_8, num_8], + }, +} + +x_9 = 3.14 +y_9 = 1.23e45 +z_9 = 0.5 + +my_str_9 : Str +my_str_9 = "one" + +binops_9 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_9 : U64 -> U64 +add_one_9 = |n| n + 1 + +map_add_one_9 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_9 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_9 = |arg_one, arg_two| arg_one * arg_two + +num_9 = 42 +frac_9 = 4.2 +str_9 = "hello" + +# Polymorphic empty collections +empty_list_9 = [] + +# Mixed polymorphic structures +mixed_9 = { + numbers: { value: num_9, list: [num_9, num_9], float: frac }, + strings: { value: str_9, list: [str_9, str_9] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_9 }, + }, + computations: { + from_num: num_9 * 100, + from_frac: frac_9 * 10.0, + list_from_num: [num_9, num_9, num_9], + }, +} + +x_10 = 3.14 +y_10 = 1.23e45 +z_10 = 0.5 + +my_str_10 : Str +my_str_10 = "one" + +binops_10 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_10 : U64 -> U64 +add_one_10 = |n| n + 1 + +map_add_one_10 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_10 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_10 = |arg_one, arg_two| arg_one * arg_two + +num_10 = 42 +frac_10 = 4.2 +str_10 = "hello" + +# Polymorphic empty collections +empty_list_10 = [] + +# Mixed polymorphic structures +mixed_10 = { + numbers: { value: num_10, list: [num_10, num_10], float: frac }, + strings: { value: str_10, list: [str_10, str_10] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_10 }, + }, + computations: { + from_num: num_10 * 100, + from_frac: frac_10 * 10.0, + list_from_num: [num_10, num_10, num_10], + }, +} + +x_11 = 3.14 +y_11 = 1.23e45 +z_11 = 0.5 + +my_str_11 : Str +my_str_11 = "one" + +binops_11 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_11 : U64 -> U64 +add_one_11 = |n| n + 1 + +map_add_one_11 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_11 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_11 = |arg_one, arg_two| arg_one * arg_two + +num_11 = 42 +frac_11 = 4.2 +str_11 = "hello" + +# Polymorphic empty collections +empty_list_11 = [] + +# Mixed polymorphic structures +mixed_11 = { + numbers: { value: num_11, list: [num_11, num_11], float: frac }, + strings: { value: str_11, list: [str_11, str_11] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_11 }, + }, + computations: { + from_num: num_11 * 100, + from_frac: frac_11 * 10.0, + list_from_num: [num_11, num_11, num_11], + }, +} + +x_12 = 3.14 +y_12 = 1.23e45 +z_12 = 0.5 + +my_str_12 : Str +my_str_12 = "one" + +binops_12 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_12 : U64 -> U64 +add_one_12 = |n| n + 1 + +map_add_one_12 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_12 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_12 = |arg_one, arg_two| arg_one * arg_two + +num_12 = 42 +frac_12 = 4.2 +str_12 = "hello" + +# Polymorphic empty collections +empty_list_12 = [] + +# Mixed polymorphic structures +mixed_12 = { + numbers: { value: num_12, list: [num_12, num_12], float: frac }, + strings: { value: str_12, list: [str_12, str_12] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_12 }, + }, + computations: { + from_num: num_12 * 100, + from_frac: frac_12 * 10.0, + list_from_num: [num_12, num_12, num_12], + }, +} + +x_13 = 3.14 +y_13 = 1.23e45 +z_13 = 0.5 + +my_str_13 : Str +my_str_13 = "one" + +binops_13 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_13 : U64 -> U64 +add_one_13 = |n| n + 1 + +map_add_one_13 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_13 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_13 = |arg_one, arg_two| arg_one * arg_two + +num_13 = 42 +frac_13 = 4.2 +str_13 = "hello" + +# Polymorphic empty collections +empty_list_13 = [] + +# Mixed polymorphic structures +mixed_13 = { + numbers: { value: num_13, list: [num_13, num_13], float: frac }, + strings: { value: str_13, list: [str_13, str_13] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_13 }, + }, + computations: { + from_num: num_13 * 100, + from_frac: frac_13 * 10.0, + list_from_num: [num_13, num_13, num_13], + }, +} + +x_14 = 3.14 +y_14 = 1.23e45 +z_14 = 0.5 + +my_str_14 : Str +my_str_14 = "one" + +binops_14 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_14 : U64 -> U64 +add_one_14 = |n| n + 1 + +map_add_one_14 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_14 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_14 = |arg_one, arg_two| arg_one * arg_two + +num_14 = 42 +frac_14 = 4.2 +str_14 = "hello" + +# Polymorphic empty collections +empty_list_14 = [] + +# Mixed polymorphic structures +mixed_14 = { + numbers: { value: num_14, list: [num_14, num_14], float: frac }, + strings: { value: str_14, list: [str_14, str_14] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_14 }, + }, + computations: { + from_num: num_14 * 100, + from_frac: frac_14 * 10.0, + list_from_num: [num_14, num_14, num_14], + }, +} + +x_15 = 3.14 +y_15 = 1.23e45 +z_15 = 0.5 + +my_str_15 : Str +my_str_15 = "one" + +binops_15 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_15 : U64 -> U64 +add_one_15 = |n| n + 1 + +map_add_one_15 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_15 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_15 = |arg_one, arg_two| arg_one * arg_two + +num_15 = 42 +frac_15 = 4.2 +str_15 = "hello" + +# Polymorphic empty collections +empty_list_15 = [] + +# Mixed polymorphic structures +mixed_15 = { + numbers: { value: num_15, list: [num_15, num_15], float: frac }, + strings: { value: str_15, list: [str_15, str_15] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_15 }, + }, + computations: { + from_num: num_15 * 100, + from_frac: frac_15 * 10.0, + list_from_num: [num_15, num_15, num_15], + }, +} + +x_16 = 3.14 +y_16 = 1.23e45 +z_16 = 0.5 + +my_str_16 : Str +my_str_16 = "one" + +binops_16 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_16 : U64 -> U64 +add_one_16 = |n| n + 1 + +map_add_one_16 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_16 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_16 = |arg_one, arg_two| arg_one * arg_two + +num_16 = 42 +frac_16 = 4.2 +str_16 = "hello" + +# Polymorphic empty collections +empty_list_16 = [] + +# Mixed polymorphic structures +mixed_16 = { + numbers: { value: num_16, list: [num_16, num_16], float: frac }, + strings: { value: str_16, list: [str_16, str_16] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_16 }, + }, + computations: { + from_num: num_16 * 100, + from_frac: frac_16 * 10.0, + list_from_num: [num_16, num_16, num_16], + }, +} + +x_17 = 3.14 +y_17 = 1.23e45 +z_17 = 0.5 + +my_str_17 : Str +my_str_17 = "one" + +binops_17 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_17 : U64 -> U64 +add_one_17 = |n| n + 1 + +map_add_one_17 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_17 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_17 = |arg_one, arg_two| arg_one * arg_two + +num_17 = 42 +frac_17 = 4.2 +str_17 = "hello" + +# Polymorphic empty collections +empty_list_17 = [] + +# Mixed polymorphic structures +mixed_17 = { + numbers: { value: num_17, list: [num_17, num_17], float: frac }, + strings: { value: str_17, list: [str_17, str_17] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_17 }, + }, + computations: { + from_num: num_17 * 100, + from_frac: frac_17 * 10.0, + list_from_num: [num_17, num_17, num_17], + }, +} + +x_18 = 3.14 +y_18 = 1.23e45 +z_18 = 0.5 + +my_str_18 : Str +my_str_18 = "one" + +binops_18 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_18 : U64 -> U64 +add_one_18 = |n| n + 1 + +map_add_one_18 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_18 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_18 = |arg_one, arg_two| arg_one * arg_two + +num_18 = 42 +frac_18 = 4.2 +str_18 = "hello" + +# Polymorphic empty collections +empty_list_18 = [] + +# Mixed polymorphic structures +mixed_18 = { + numbers: { value: num_18, list: [num_18, num_18], float: frac }, + strings: { value: str_18, list: [str_18, str_18] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_18 }, + }, + computations: { + from_num: num_18 * 100, + from_frac: frac_18 * 10.0, + list_from_num: [num_18, num_18, num_18], + }, +} + +x_19 = 3.14 +y_19 = 1.23e45 +z_19 = 0.5 + +my_str_19 : Str +my_str_19 = "one" + +binops_19 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_19 : U64 -> U64 +add_one_19 = |n| n + 1 + +map_add_one_19 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_19 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_19 = |arg_one, arg_two| arg_one * arg_two + +num_19 = 42 +frac_19 = 4.2 +str_19 = "hello" + +# Polymorphic empty collections +empty_list_19 = [] + +# Mixed polymorphic structures +mixed_19 = { + numbers: { value: num_19, list: [num_19, num_19], float: frac }, + strings: { value: str_19, list: [str_19, str_19] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_19 }, + }, + computations: { + from_num: num_19 * 100, + from_frac: frac_19 * 10.0, + list_from_num: [num_19, num_19, num_19], + }, +} + +x_20 = 3.14 +y_20 = 1.23e45 +z_20 = 0.5 + +my_str_20 : Str +my_str_20 = "one" + +binops_20 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_20 : U64 -> U64 +add_one_20 = |n| n + 1 + +map_add_one_20 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_20 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_20 = |arg_one, arg_two| arg_one * arg_two + +num_20 = 42 +frac_20 = 4.2 +str_20 = "hello" + +# Polymorphic empty collections +empty_list_20 = [] + +# Mixed polymorphic structures +mixed_20 = { + numbers: { value: num_20, list: [num_20, num_20], float: frac }, + strings: { value: str_20, list: [str_20, str_20] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_20 }, + }, + computations: { + from_num: num_20 * 100, + from_frac: frac_20 * 10.0, + list_from_num: [num_20, num_20, num_20], + }, +} + +x_21 = 3.14 +y_21 = 1.23e45 +z_21 = 0.5 + +my_str_21 : Str +my_str_21 = "one" + +binops_21 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_21 : U64 -> U64 +add_one_21 = |n| n + 1 + +map_add_one_21 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_21 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_21 = |arg_one, arg_two| arg_one * arg_two + +num_21 = 42 +frac_21 = 4.2 +str_21 = "hello" + +# Polymorphic empty collections +empty_list_21 = [] + +# Mixed polymorphic structures +mixed_21 = { + numbers: { value: num_21, list: [num_21, num_21], float: frac }, + strings: { value: str_21, list: [str_21, str_21] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_21 }, + }, + computations: { + from_num: num_21 * 100, + from_frac: frac_21 * 10.0, + list_from_num: [num_21, num_21, num_21], + }, +} + +x_22 = 3.14 +y_22 = 1.23e45 +z_22 = 0.5 + +my_str_22 : Str +my_str_22 = "one" + +binops_22 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_22 : U64 -> U64 +add_one_22 = |n| n + 1 + +map_add_one_22 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_22 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_22 = |arg_one, arg_two| arg_one * arg_two + +num_22 = 42 +frac_22 = 4.2 +str_22 = "hello" + +# Polymorphic empty collections +empty_list_22 = [] + +# Mixed polymorphic structures +mixed_22 = { + numbers: { value: num_22, list: [num_22, num_22], float: frac }, + strings: { value: str_22, list: [str_22, str_22] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_22 }, + }, + computations: { + from_num: num_22 * 100, + from_frac: frac_22 * 10.0, + list_from_num: [num_22, num_22, num_22], + }, +} + +x_23 = 3.14 +y_23 = 1.23e45 +z_23 = 0.5 + +my_str_23 : Str +my_str_23 = "one" + +binops_23 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_23 : U64 -> U64 +add_one_23 = |n| n + 1 + +map_add_one_23 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_23 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_23 = |arg_one, arg_two| arg_one * arg_two + +num_23 = 42 +frac_23 = 4.2 +str_23 = "hello" + +# Polymorphic empty collections +empty_list_23 = [] + +# Mixed polymorphic structures +mixed_23 = { + numbers: { value: num_23, list: [num_23, num_23], float: frac }, + strings: { value: str_23, list: [str_23, str_23] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_23 }, + }, + computations: { + from_num: num_23 * 100, + from_frac: frac_23 * 10.0, + list_from_num: [num_23, num_23, num_23], + }, +} + +x_24 = 3.14 +y_24 = 1.23e45 +z_24 = 0.5 + +my_str_24 : Str +my_str_24 = "one" + +binops_24 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_24 : U64 -> U64 +add_one_24 = |n| n + 1 + +map_add_one_24 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_24 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_24 = |arg_one, arg_two| arg_one * arg_two + +num_24 = 42 +frac_24 = 4.2 +str_24 = "hello" + +# Polymorphic empty collections +empty_list_24 = [] + +# Mixed polymorphic structures +mixed_24 = { + numbers: { value: num_24, list: [num_24, num_24], float: frac }, + strings: { value: str_24, list: [str_24, str_24] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_24 }, + }, + computations: { + from_num: num_24 * 100, + from_frac: frac_24 * 10.0, + list_from_num: [num_24, num_24, num_24], + }, +} + +x_25 = 3.14 +y_25 = 1.23e45 +z_25 = 0.5 + +my_str_25 : Str +my_str_25 = "one" + +binops_25 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_25 : U64 -> U64 +add_one_25 = |n| n + 1 + +map_add_one_25 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_25 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_25 = |arg_one, arg_two| arg_one * arg_two + +num_25 = 42 +frac_25 = 4.2 +str_25 = "hello" + +# Polymorphic empty collections +empty_list_25 = [] + +# Mixed polymorphic structures +mixed_25 = { + numbers: { value: num_25, list: [num_25, num_25], float: frac }, + strings: { value: str_25, list: [str_25, str_25] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_25 }, + }, + computations: { + from_num: num_25 * 100, + from_frac: frac_25 * 10.0, + list_from_num: [num_25, num_25, num_25], + }, +} + +x_26 = 3.14 +y_26 = 1.23e45 +z_26 = 0.5 + +my_str_26 : Str +my_str_26 = "one" + +binops_26 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_26 : U64 -> U64 +add_one_26 = |n| n + 1 + +map_add_one_26 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_26 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_26 = |arg_one, arg_two| arg_one * arg_two + +num_26 = 42 +frac_26 = 4.2 +str_26 = "hello" + +# Polymorphic empty collections +empty_list_26 = [] + +# Mixed polymorphic structures +mixed_26 = { + numbers: { value: num_26, list: [num_26, num_26], float: frac }, + strings: { value: str_26, list: [str_26, str_26] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_26 }, + }, + computations: { + from_num: num_26 * 100, + from_frac: frac_26 * 10.0, + list_from_num: [num_26, num_26, num_26], + }, +} + +x_27 = 3.14 +y_27 = 1.23e45 +z_27 = 0.5 + +my_str_27 : Str +my_str_27 = "one" + +binops_27 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_27 : U64 -> U64 +add_one_27 = |n| n + 1 + +map_add_one_27 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_27 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_27 = |arg_one, arg_two| arg_one * arg_two + +num_27 = 42 +frac_27 = 4.2 +str_27 = "hello" + +# Polymorphic empty collections +empty_list_27 = [] + +# Mixed polymorphic structures +mixed_27 = { + numbers: { value: num_27, list: [num_27, num_27], float: frac }, + strings: { value: str_27, list: [str_27, str_27] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_27 }, + }, + computations: { + from_num: num_27 * 100, + from_frac: frac_27 * 10.0, + list_from_num: [num_27, num_27, num_27], + }, +} + +x_28 = 3.14 +y_28 = 1.23e45 +z_28 = 0.5 + +my_str_28 : Str +my_str_28 = "one" + +binops_28 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_28 : U64 -> U64 +add_one_28 = |n| n + 1 + +map_add_one_28 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_28 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_28 = |arg_one, arg_two| arg_one * arg_two + +num_28 = 42 +frac_28 = 4.2 +str_28 = "hello" + +# Polymorphic empty collections +empty_list_28 = [] + +# Mixed polymorphic structures +mixed_28 = { + numbers: { value: num_28, list: [num_28, num_28], float: frac }, + strings: { value: str_28, list: [str_28, str_28] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_28 }, + }, + computations: { + from_num: num_28 * 100, + from_frac: frac_28 * 10.0, + list_from_num: [num_28, num_28, num_28], + }, +} + +x_29 = 3.14 +y_29 = 1.23e45 +z_29 = 0.5 + +my_str_29 : Str +my_str_29 = "one" + +binops_29 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_29 : U64 -> U64 +add_one_29 = |n| n + 1 + +map_add_one_29 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_29 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_29 = |arg_one, arg_two| arg_one * arg_two + +num_29 = 42 +frac_29 = 4.2 +str_29 = "hello" + +# Polymorphic empty collections +empty_list_29 = [] + +# Mixed polymorphic structures +mixed_29 = { + numbers: { value: num_29, list: [num_29, num_29], float: frac }, + strings: { value: str_29, list: [str_29, str_29] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_29 }, + }, + computations: { + from_num: num_29 * 100, + from_frac: frac_29 * 10.0, + list_from_num: [num_29, num_29, num_29], + }, +} + +x_30 = 3.14 +y_30 = 1.23e45 +z_30 = 0.5 + +my_str_30 : Str +my_str_30 = "one" + +binops_30 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_30 : U64 -> U64 +add_one_30 = |n| n + 1 + +map_add_one_30 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_30 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_30 = |arg_one, arg_two| arg_one * arg_two + +num_30 = 42 +frac_30 = 4.2 +str_30 = "hello" + +# Polymorphic empty collections +empty_list_30 = [] + +# Mixed polymorphic structures +mixed_30 = { + numbers: { value: num_30, list: [num_30, num_30], float: frac }, + strings: { value: str_30, list: [str_30, str_30] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_30 }, + }, + computations: { + from_num: num_30 * 100, + from_frac: frac_30 * 10.0, + list_from_num: [num_30, num_30, num_30], + }, +} + +x_31 = 3.14 +y_31 = 1.23e45 +z_31 = 0.5 + +my_str_31 : Str +my_str_31 = "one" + +binops_31 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_31 : U64 -> U64 +add_one_31 = |n| n + 1 + +map_add_one_31 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_31 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_31 = |arg_one, arg_two| arg_one * arg_two + +num_31 = 42 +frac_31 = 4.2 +str_31 = "hello" + +# Polymorphic empty collections +empty_list_31 = [] + +# Mixed polymorphic structures +mixed_31 = { + numbers: { value: num_31, list: [num_31, num_31], float: frac }, + strings: { value: str_31, list: [str_31, str_31] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_31 }, + }, + computations: { + from_num: num_31 * 100, + from_frac: frac_31 * 10.0, + list_from_num: [num_31, num_31, num_31], + }, +} + +x_32 = 3.14 +y_32 = 1.23e45 +z_32 = 0.5 + +my_str_32 : Str +my_str_32 = "one" + +binops_32 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_32 : U64 -> U64 +add_one_32 = |n| n + 1 + +map_add_one_32 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_32 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_32 = |arg_one, arg_two| arg_one * arg_two + +num_32 = 42 +frac_32 = 4.2 +str_32 = "hello" + +# Polymorphic empty collections +empty_list_32 = [] + +# Mixed polymorphic structures +mixed_32 = { + numbers: { value: num_32, list: [num_32, num_32], float: frac }, + strings: { value: str_32, list: [str_32, str_32] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_32 }, + }, + computations: { + from_num: num_32 * 100, + from_frac: frac_32 * 10.0, + list_from_num: [num_32, num_32, num_32], + }, +} + +x_33 = 3.14 +y_33 = 1.23e45 +z_33 = 0.5 + +my_str_33 : Str +my_str_33 = "one" + +binops_33 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_33 : U64 -> U64 +add_one_33 = |n| n + 1 + +map_add_one_33 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_33 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_33 = |arg_one, arg_two| arg_one * arg_two + +num_33 = 42 +frac_33 = 4.2 +str_33 = "hello" + +# Polymorphic empty collections +empty_list_33 = [] + +# Mixed polymorphic structures +mixed_33 = { + numbers: { value: num_33, list: [num_33, num_33], float: frac }, + strings: { value: str_33, list: [str_33, str_33] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_33 }, + }, + computations: { + from_num: num_33 * 100, + from_frac: frac_33 * 10.0, + list_from_num: [num_33, num_33, num_33], + }, +} + +x_34 = 3.14 +y_34 = 1.23e45 +z_34 = 0.5 + +my_str_34 : Str +my_str_34 = "one" + +binops_34 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_34 : U64 -> U64 +add_one_34 = |n| n + 1 + +map_add_one_34 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_34 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_34 = |arg_one, arg_two| arg_one * arg_two + +num_34 = 42 +frac_34 = 4.2 +str_34 = "hello" + +# Polymorphic empty collections +empty_list_34 = [] + +# Mixed polymorphic structures +mixed_34 = { + numbers: { value: num_34, list: [num_34, num_34], float: frac }, + strings: { value: str_34, list: [str_34, str_34] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_34 }, + }, + computations: { + from_num: num_34 * 100, + from_frac: frac_34 * 10.0, + list_from_num: [num_34, num_34, num_34], + }, +} + +x_35 = 3.14 +y_35 = 1.23e45 +z_35 = 0.5 + +my_str_35 : Str +my_str_35 = "one" + +binops_35 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_35 : U64 -> U64 +add_one_35 = |n| n + 1 + +map_add_one_35 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_35 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_35 = |arg_one, arg_two| arg_one * arg_two + +num_35 = 42 +frac_35 = 4.2 +str_35 = "hello" + +# Polymorphic empty collections +empty_list_35 = [] + +# Mixed polymorphic structures +mixed_35 = { + numbers: { value: num_35, list: [num_35, num_35], float: frac }, + strings: { value: str_35, list: [str_35, str_35] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_35 }, + }, + computations: { + from_num: num_35 * 100, + from_frac: frac_35 * 10.0, + list_from_num: [num_35, num_35, num_35], + }, +} + +x_36 = 3.14 +y_36 = 1.23e45 +z_36 = 0.5 + +my_str_36 : Str +my_str_36 = "one" + +binops_36 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_36 : U64 -> U64 +add_one_36 = |n| n + 1 + +map_add_one_36 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_36 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_36 = |arg_one, arg_two| arg_one * arg_two + +num_36 = 42 +frac_36 = 4.2 +str_36 = "hello" + +# Polymorphic empty collections +empty_list_36 = [] + +# Mixed polymorphic structures +mixed_36 = { + numbers: { value: num_36, list: [num_36, num_36], float: frac }, + strings: { value: str_36, list: [str_36, str_36] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_36 }, + }, + computations: { + from_num: num_36 * 100, + from_frac: frac_36 * 10.0, + list_from_num: [num_36, num_36, num_36], + }, +} + +x_37 = 3.14 +y_37 = 1.23e45 +z_37 = 0.5 + +my_str_37 : Str +my_str_37 = "one" + +binops_37 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_37 : U64 -> U64 +add_one_37 = |n| n + 1 + +map_add_one_37 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_37 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_37 = |arg_one, arg_two| arg_one * arg_two + +num_37 = 42 +frac_37 = 4.2 +str_37 = "hello" + +# Polymorphic empty collections +empty_list_37 = [] + +# Mixed polymorphic structures +mixed_37 = { + numbers: { value: num_37, list: [num_37, num_37], float: frac }, + strings: { value: str_37, list: [str_37, str_37] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_37 }, + }, + computations: { + from_num: num_37 * 100, + from_frac: frac_37 * 10.0, + list_from_num: [num_37, num_37, num_37], + }, +} + +x_38 = 3.14 +y_38 = 1.23e45 +z_38 = 0.5 + +my_str_38 : Str +my_str_38 = "one" + +binops_38 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_38 : U64 -> U64 +add_one_38 = |n| n + 1 + +map_add_one_38 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_38 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_38 = |arg_one, arg_two| arg_one * arg_two + +num_38 = 42 +frac_38 = 4.2 +str_38 = "hello" + +# Polymorphic empty collections +empty_list_38 = [] + +# Mixed polymorphic structures +mixed_38 = { + numbers: { value: num_38, list: [num_38, num_38], float: frac }, + strings: { value: str_38, list: [str_38, str_38] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_38 }, + }, + computations: { + from_num: num_38 * 100, + from_frac: frac_38 * 10.0, + list_from_num: [num_38, num_38, num_38], + }, +} + +x_39 = 3.14 +y_39 = 1.23e45 +z_39 = 0.5 + +my_str_39 : Str +my_str_39 = "one" + +binops_39 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_39 : U64 -> U64 +add_one_39 = |n| n + 1 + +map_add_one_39 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_39 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_39 = |arg_one, arg_two| arg_one * arg_two + +num_39 = 42 +frac_39 = 4.2 +str_39 = "hello" + +# Polymorphic empty collections +empty_list_39 = [] + +# Mixed polymorphic structures +mixed_39 = { + numbers: { value: num_39, list: [num_39, num_39], float: frac }, + strings: { value: str_39, list: [str_39, str_39] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_39 }, + }, + computations: { + from_num: num_39 * 100, + from_frac: frac_39 * 10.0, + list_from_num: [num_39, num_39, num_39], + }, +} + +x_40 = 3.14 +y_40 = 1.23e45 +z_40 = 0.5 + +my_str_40 : Str +my_str_40 = "one" + +binops_40 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_40 : U64 -> U64 +add_one_40 = |n| n + 1 + +map_add_one_40 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_40 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_40 = |arg_one, arg_two| arg_one * arg_two + +num_40 = 42 +frac_40 = 4.2 +str_40 = "hello" + +# Polymorphic empty collections +empty_list_40 = [] + +# Mixed polymorphic structures +mixed_40 = { + numbers: { value: num_40, list: [num_40, num_40], float: frac }, + strings: { value: str_40, list: [str_40, str_40] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_40 }, + }, + computations: { + from_num: num_40 * 100, + from_frac: frac_40 * 10.0, + list_from_num: [num_40, num_40, num_40], + }, +} + +x_41 = 3.14 +y_41 = 1.23e45 +z_41 = 0.5 + +my_str_41 : Str +my_str_41 = "one" + +binops_41 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_41 : U64 -> U64 +add_one_41 = |n| n + 1 + +map_add_one_41 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_41 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_41 = |arg_one, arg_two| arg_one * arg_two + +num_41 = 42 +frac_41 = 4.2 +str_41 = "hello" + +# Polymorphic empty collections +empty_list_41 = [] + +# Mixed polymorphic structures +mixed_41 = { + numbers: { value: num_41, list: [num_41, num_41], float: frac }, + strings: { value: str_41, list: [str_41, str_41] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_41 }, + }, + computations: { + from_num: num_41 * 100, + from_frac: frac_41 * 10.0, + list_from_num: [num_41, num_41, num_41], + }, +} + +x_42 = 3.14 +y_42 = 1.23e45 +z_42 = 0.5 + +my_str_42 : Str +my_str_42 = "one" + +binops_42 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_42 : U64 -> U64 +add_one_42 = |n| n + 1 + +map_add_one_42 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_42 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_42 = |arg_one, arg_two| arg_one * arg_two + +num_42 = 42 +frac_42 = 4.2 +str_42 = "hello" + +# Polymorphic empty collections +empty_list_42 = [] + +# Mixed polymorphic structures +mixed_42 = { + numbers: { value: num_42, list: [num_42, num_42], float: frac }, + strings: { value: str_42, list: [str_42, str_42] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_42 }, + }, + computations: { + from_num: num_42 * 100, + from_frac: frac_42 * 10.0, + list_from_num: [num_42, num_42, num_42], + }, +} + +x_43 = 3.14 +y_43 = 1.23e45 +z_43 = 0.5 + +my_str_43 : Str +my_str_43 = "one" + +binops_43 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_43 : U64 -> U64 +add_one_43 = |n| n + 1 + +map_add_one_43 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_43 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_43 = |arg_one, arg_two| arg_one * arg_two + +num_43 = 42 +frac_43 = 4.2 +str_43 = "hello" + +# Polymorphic empty collections +empty_list_43 = [] + +# Mixed polymorphic structures +mixed_43 = { + numbers: { value: num_43, list: [num_43, num_43], float: frac }, + strings: { value: str_43, list: [str_43, str_43] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_43 }, + }, + computations: { + from_num: num_43 * 100, + from_frac: frac_43 * 10.0, + list_from_num: [num_43, num_43, num_43], + }, +} + +x_44 = 3.14 +y_44 = 1.23e45 +z_44 = 0.5 + +my_str_44 : Str +my_str_44 = "one" + +binops_44 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_44 : U64 -> U64 +add_one_44 = |n| n + 1 + +map_add_one_44 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_44 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_44 = |arg_one, arg_two| arg_one * arg_two + +num_44 = 42 +frac_44 = 4.2 +str_44 = "hello" + +# Polymorphic empty collections +empty_list_44 = [] + +# Mixed polymorphic structures +mixed_44 = { + numbers: { value: num_44, list: [num_44, num_44], float: frac }, + strings: { value: str_44, list: [str_44, str_44] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_44 }, + }, + computations: { + from_num: num_44 * 100, + from_frac: frac_44 * 10.0, + list_from_num: [num_44, num_44, num_44], + }, +} + +x_45 = 3.14 +y_45 = 1.23e45 +z_45 = 0.5 + +my_str_45 : Str +my_str_45 = "one" + +binops_45 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_45 : U64 -> U64 +add_one_45 = |n| n + 1 + +map_add_one_45 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_45 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_45 = |arg_one, arg_two| arg_one * arg_two + +num_45 = 42 +frac_45 = 4.2 +str_45 = "hello" + +# Polymorphic empty collections +empty_list_45 = [] + +# Mixed polymorphic structures +mixed_45 = { + numbers: { value: num_45, list: [num_45, num_45], float: frac }, + strings: { value: str_45, list: [str_45, str_45] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_45 }, + }, + computations: { + from_num: num_45 * 100, + from_frac: frac_45 * 10.0, + list_from_num: [num_45, num_45, num_45], + }, +} + +x_46 = 3.14 +y_46 = 1.23e45 +z_46 = 0.5 + +my_str_46 : Str +my_str_46 = "one" + +binops_46 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_46 : U64 -> U64 +add_one_46 = |n| n + 1 + +map_add_one_46 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_46 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_46 = |arg_one, arg_two| arg_one * arg_two + +num_46 = 42 +frac_46 = 4.2 +str_46 = "hello" + +# Polymorphic empty collections +empty_list_46 = [] + +# Mixed polymorphic structures +mixed_46 = { + numbers: { value: num_46, list: [num_46, num_46], float: frac }, + strings: { value: str_46, list: [str_46, str_46] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_46 }, + }, + computations: { + from_num: num_46 * 100, + from_frac: frac_46 * 10.0, + list_from_num: [num_46, num_46, num_46], + }, +} + +x_47 = 3.14 +y_47 = 1.23e45 +z_47 = 0.5 + +my_str_47 : Str +my_str_47 = "one" + +binops_47 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_47 : U64 -> U64 +add_one_47 = |n| n + 1 + +map_add_one_47 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_47 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_47 = |arg_one, arg_two| arg_one * arg_two + +num_47 = 42 +frac_47 = 4.2 +str_47 = "hello" + +# Polymorphic empty collections +empty_list_47 = [] + +# Mixed polymorphic structures +mixed_47 = { + numbers: { value: num_47, list: [num_47, num_47], float: frac }, + strings: { value: str_47, list: [str_47, str_47] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_47 }, + }, + computations: { + from_num: num_47 * 100, + from_frac: frac_47 * 10.0, + list_from_num: [num_47, num_47, num_47], + }, +} + +x_48 = 3.14 +y_48 = 1.23e45 +z_48 = 0.5 + +my_str_48 : Str +my_str_48 = "one" + +binops_48 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_48 : U64 -> U64 +add_one_48 = |n| n + 1 + +map_add_one_48 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_48 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_48 = |arg_one, arg_two| arg_one * arg_two + +num_48 = 42 +frac_48 = 4.2 +str_48 = "hello" + +# Polymorphic empty collections +empty_list_48 = [] + +# Mixed polymorphic structures +mixed_48 = { + numbers: { value: num_48, list: [num_48, num_48], float: frac }, + strings: { value: str_48, list: [str_48, str_48] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_48 }, + }, + computations: { + from_num: num_48 * 100, + from_frac: frac_48 * 10.0, + list_from_num: [num_48, num_48, num_48], + }, +} + +x_49 = 3.14 +y_49 = 1.23e45 +z_49 = 0.5 + +my_str_49 : Str +my_str_49 = "one" + +binops_49 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_49 : U64 -> U64 +add_one_49 = |n| n + 1 + +map_add_one_49 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_49 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_49 = |arg_one, arg_two| arg_one * arg_two + +num_49 = 42 +frac_49 = 4.2 +str_49 = "hello" + +# Polymorphic empty collections +empty_list_49 = [] + +# Mixed polymorphic structures +mixed_49 = { + numbers: { value: num_49, list: [num_49, num_49], float: frac }, + strings: { value: str_49, list: [str_49, str_49] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_49 }, + }, + computations: { + from_num: num_49 * 100, + from_frac: frac_49 * 10.0, + list_from_num: [num_49, num_49, num_49], + }, +} + +x_50 = 3.14 +y_50 = 1.23e45 +z_50 = 0.5 + +my_str_50 : Str +my_str_50 = "one" + +binops_50 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_50 : U64 -> U64 +add_one_50 = |n| n + 1 + +map_add_one_50 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_50 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_50 = |arg_one, arg_two| arg_one * arg_two + +num_50 = 42 +frac_50 = 4.2 +str_50 = "hello" + +# Polymorphic empty collections +empty_list_50 = [] + +# Mixed polymorphic structures +mixed_50 = { + numbers: { value: num_50, list: [num_50, num_50], float: frac }, + strings: { value: str_50, list: [str_50, str_50] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_50 }, + }, + computations: { + from_num: num_50 * 100, + from_frac: frac_50 * 10.0, + list_from_num: [num_50, num_50, num_50], + }, +} + +x_51 = 3.14 +y_51 = 1.23e45 +z_51 = 0.5 + +my_str_51 : Str +my_str_51 = "one" + +binops_51 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_51 : U64 -> U64 +add_one_51 = |n| n + 1 + +map_add_one_51 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_51 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_51 = |arg_one, arg_two| arg_one * arg_two + +num_51 = 42 +frac_51 = 4.2 +str_51 = "hello" + +# Polymorphic empty collections +empty_list_51 = [] + +# Mixed polymorphic structures +mixed_51 = { + numbers: { value: num_51, list: [num_51, num_51], float: frac }, + strings: { value: str_51, list: [str_51, str_51] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_51 }, + }, + computations: { + from_num: num_51 * 100, + from_frac: frac_51 * 10.0, + list_from_num: [num_51, num_51, num_51], + }, +} + +x_52 = 3.14 +y_52 = 1.23e45 +z_52 = 0.5 + +my_str_52 : Str +my_str_52 = "one" + +binops_52 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_52 : U64 -> U64 +add_one_52 = |n| n + 1 + +map_add_one_52 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_52 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_52 = |arg_one, arg_two| arg_one * arg_two + +num_52 = 42 +frac_52 = 4.2 +str_52 = "hello" + +# Polymorphic empty collections +empty_list_52 = [] + +# Mixed polymorphic structures +mixed_52 = { + numbers: { value: num_52, list: [num_52, num_52], float: frac }, + strings: { value: str_52, list: [str_52, str_52] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_52 }, + }, + computations: { + from_num: num_52 * 100, + from_frac: frac_52 * 10.0, + list_from_num: [num_52, num_52, num_52], + }, +} + +x_53 = 3.14 +y_53 = 1.23e45 +z_53 = 0.5 + +my_str_53 : Str +my_str_53 = "one" + +binops_53 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_53 : U64 -> U64 +add_one_53 = |n| n + 1 + +map_add_one_53 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_53 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_53 = |arg_one, arg_two| arg_one * arg_two + +num_53 = 42 +frac_53 = 4.2 +str_53 = "hello" + +# Polymorphic empty collections +empty_list_53 = [] + +# Mixed polymorphic structures +mixed_53 = { + numbers: { value: num_53, list: [num_53, num_53], float: frac }, + strings: { value: str_53, list: [str_53, str_53] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_53 }, + }, + computations: { + from_num: num_53 * 100, + from_frac: frac_53 * 10.0, + list_from_num: [num_53, num_53, num_53], + }, +} + +x_54 = 3.14 +y_54 = 1.23e45 +z_54 = 0.5 + +my_str_54 : Str +my_str_54 = "one" + +binops_54 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_54 : U64 -> U64 +add_one_54 = |n| n + 1 + +map_add_one_54 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_54 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_54 = |arg_one, arg_two| arg_one * arg_two + +num_54 = 42 +frac_54 = 4.2 +str_54 = "hello" + +# Polymorphic empty collections +empty_list_54 = [] + +# Mixed polymorphic structures +mixed_54 = { + numbers: { value: num_54, list: [num_54, num_54], float: frac }, + strings: { value: str_54, list: [str_54, str_54] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_54 }, + }, + computations: { + from_num: num_54 * 100, + from_frac: frac_54 * 10.0, + list_from_num: [num_54, num_54, num_54], + }, +} + +x_55 = 3.14 +y_55 = 1.23e45 +z_55 = 0.5 + +my_str_55 : Str +my_str_55 = "one" + +binops_55 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_55 : U64 -> U64 +add_one_55 = |n| n + 1 + +map_add_one_55 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_55 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_55 = |arg_one, arg_two| arg_one * arg_two + +num_55 = 42 +frac_55 = 4.2 +str_55 = "hello" + +# Polymorphic empty collections +empty_list_55 = [] + +# Mixed polymorphic structures +mixed_55 = { + numbers: { value: num_55, list: [num_55, num_55], float: frac }, + strings: { value: str_55, list: [str_55, str_55] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_55 }, + }, + computations: { + from_num: num_55 * 100, + from_frac: frac_55 * 10.0, + list_from_num: [num_55, num_55, num_55], + }, +} + +x_56 = 3.14 +y_56 = 1.23e45 +z_56 = 0.5 + +my_str_56 : Str +my_str_56 = "one" + +binops_56 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_56 : U64 -> U64 +add_one_56 = |n| n + 1 + +map_add_one_56 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_56 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_56 = |arg_one, arg_two| arg_one * arg_two + +num_56 = 42 +frac_56 = 4.2 +str_56 = "hello" + +# Polymorphic empty collections +empty_list_56 = [] + +# Mixed polymorphic structures +mixed_56 = { + numbers: { value: num_56, list: [num_56, num_56], float: frac }, + strings: { value: str_56, list: [str_56, str_56] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_56 }, + }, + computations: { + from_num: num_56 * 100, + from_frac: frac_56 * 10.0, + list_from_num: [num_56, num_56, num_56], + }, +} + +x_57 = 3.14 +y_57 = 1.23e45 +z_57 = 0.5 + +my_str_57 : Str +my_str_57 = "one" + +binops_57 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_57 : U64 -> U64 +add_one_57 = |n| n + 1 + +map_add_one_57 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_57 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_57 = |arg_one, arg_two| arg_one * arg_two + +num_57 = 42 +frac_57 = 4.2 +str_57 = "hello" + +# Polymorphic empty collections +empty_list_57 = [] + +# Mixed polymorphic structures +mixed_57 = { + numbers: { value: num_57, list: [num_57, num_57], float: frac }, + strings: { value: str_57, list: [str_57, str_57] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_57 }, + }, + computations: { + from_num: num_57 * 100, + from_frac: frac_57 * 10.0, + list_from_num: [num_57, num_57, num_57], + }, +} + +x_58 = 3.14 +y_58 = 1.23e45 +z_58 = 0.5 + +my_str_58 : Str +my_str_58 = "one" + +binops_58 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_58 : U64 -> U64 +add_one_58 = |n| n + 1 + +map_add_one_58 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_58 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_58 = |arg_one, arg_two| arg_one * arg_two + +num_58 = 42 +frac_58 = 4.2 +str_58 = "hello" + +# Polymorphic empty collections +empty_list_58 = [] + +# Mixed polymorphic structures +mixed_58 = { + numbers: { value: num_58, list: [num_58, num_58], float: frac }, + strings: { value: str_58, list: [str_58, str_58] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_58 }, + }, + computations: { + from_num: num_58 * 100, + from_frac: frac_58 * 10.0, + list_from_num: [num_58, num_58, num_58], + }, +} + +x_59 = 3.14 +y_59 = 1.23e45 +z_59 = 0.5 + +my_str_59 : Str +my_str_59 = "one" + +binops_59 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_59 : U64 -> U64 +add_one_59 = |n| n + 1 + +map_add_one_59 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_59 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_59 = |arg_one, arg_two| arg_one * arg_two + +num_59 = 42 +frac_59 = 4.2 +str_59 = "hello" + +# Polymorphic empty collections +empty_list_59 = [] + +# Mixed polymorphic structures +mixed_59 = { + numbers: { value: num_59, list: [num_59, num_59], float: frac }, + strings: { value: str_59, list: [str_59, str_59] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_59 }, + }, + computations: { + from_num: num_59 * 100, + from_frac: frac_59 * 10.0, + list_from_num: [num_59, num_59, num_59], + }, +} + +x_60 = 3.14 +y_60 = 1.23e45 +z_60 = 0.5 + +my_str_60 : Str +my_str_60 = "one" + +binops_60 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_60 : U64 -> U64 +add_one_60 = |n| n + 1 + +map_add_one_60 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_60 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_60 = |arg_one, arg_two| arg_one * arg_two + +num_60 = 42 +frac_60 = 4.2 +str_60 = "hello" + +# Polymorphic empty collections +empty_list_60 = [] + +# Mixed polymorphic structures +mixed_60 = { + numbers: { value: num_60, list: [num_60, num_60], float: frac }, + strings: { value: str_60, list: [str_60, str_60] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_60 }, + }, + computations: { + from_num: num_60 * 100, + from_frac: frac_60 * 10.0, + list_from_num: [num_60, num_60, num_60], + }, +} + +x_61 = 3.14 +y_61 = 1.23e45 +z_61 = 0.5 + +my_str_61 : Str +my_str_61 = "one" + +binops_61 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_61 : U64 -> U64 +add_one_61 = |n| n + 1 + +map_add_one_61 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_61 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_61 = |arg_one, arg_two| arg_one * arg_two + +num_61 = 42 +frac_61 = 4.2 +str_61 = "hello" + +# Polymorphic empty collections +empty_list_61 = [] + +# Mixed polymorphic structures +mixed_61 = { + numbers: { value: num_61, list: [num_61, num_61], float: frac }, + strings: { value: str_61, list: [str_61, str_61] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_61 }, + }, + computations: { + from_num: num_61 * 100, + from_frac: frac_61 * 10.0, + list_from_num: [num_61, num_61, num_61], + }, +} + +x_62 = 3.14 +y_62 = 1.23e45 +z_62 = 0.5 + +my_str_62 : Str +my_str_62 = "one" + +binops_62 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_62 : U64 -> U64 +add_one_62 = |n| n + 1 + +map_add_one_62 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_62 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_62 = |arg_one, arg_two| arg_one * arg_two + +num_62 = 42 +frac_62 = 4.2 +str_62 = "hello" + +# Polymorphic empty collections +empty_list_62 = [] + +# Mixed polymorphic structures +mixed_62 = { + numbers: { value: num_62, list: [num_62, num_62], float: frac }, + strings: { value: str_62, list: [str_62, str_62] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_62 }, + }, + computations: { + from_num: num_62 * 100, + from_frac: frac_62 * 10.0, + list_from_num: [num_62, num_62, num_62], + }, +} + +x_63 = 3.14 +y_63 = 1.23e45 +z_63 = 0.5 + +my_str_63 : Str +my_str_63 = "one" + +binops_63 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_63 : U64 -> U64 +add_one_63 = |n| n + 1 + +map_add_one_63 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_63 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_63 = |arg_one, arg_two| arg_one * arg_two + +num_63 = 42 +frac_63 = 4.2 +str_63 = "hello" + +# Polymorphic empty collections +empty_list_63 = [] + +# Mixed polymorphic structures +mixed_63 = { + numbers: { value: num_63, list: [num_63, num_63], float: frac }, + strings: { value: str_63, list: [str_63, str_63] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_63 }, + }, + computations: { + from_num: num_63 * 100, + from_frac: frac_63 * 10.0, + list_from_num: [num_63, num_63, num_63], + }, +} + +x_64 = 3.14 +y_64 = 1.23e45 +z_64 = 0.5 + +my_str_64 : Str +my_str_64 = "one" + +binops_64 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_64 : U64 -> U64 +add_one_64 = |n| n + 1 + +map_add_one_64 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_64 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_64 = |arg_one, arg_two| arg_one * arg_two + +num_64 = 42 +frac_64 = 4.2 +str_64 = "hello" + +# Polymorphic empty collections +empty_list_64 = [] + +# Mixed polymorphic structures +mixed_64 = { + numbers: { value: num_64, list: [num_64, num_64], float: frac }, + strings: { value: str_64, list: [str_64, str_64] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_64 }, + }, + computations: { + from_num: num_64 * 100, + from_frac: frac_64 * 10.0, + list_from_num: [num_64, num_64, num_64], + }, +} + +x_65 = 3.14 +y_65 = 1.23e45 +z_65 = 0.5 + +my_str_65 : Str +my_str_65 = "one" + +binops_65 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_65 : U64 -> U64 +add_one_65 = |n| n + 1 + +map_add_one_65 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_65 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_65 = |arg_one, arg_two| arg_one * arg_two + +num_65 = 42 +frac_65 = 4.2 +str_65 = "hello" + +# Polymorphic empty collections +empty_list_65 = [] + +# Mixed polymorphic structures +mixed_65 = { + numbers: { value: num_65, list: [num_65, num_65], float: frac }, + strings: { value: str_65, list: [str_65, str_65] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_65 }, + }, + computations: { + from_num: num_65 * 100, + from_frac: frac_65 * 10.0, + list_from_num: [num_65, num_65, num_65], + }, +} + +x_66 = 3.14 +y_66 = 1.23e45 +z_66 = 0.5 + +my_str_66 : Str +my_str_66 = "one" + +binops_66 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_66 : U64 -> U64 +add_one_66 = |n| n + 1 + +map_add_one_66 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_66 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_66 = |arg_one, arg_two| arg_one * arg_two + +num_66 = 42 +frac_66 = 4.2 +str_66 = "hello" + +# Polymorphic empty collections +empty_list_66 = [] + +# Mixed polymorphic structures +mixed_66 = { + numbers: { value: num_66, list: [num_66, num_66], float: frac }, + strings: { value: str_66, list: [str_66, str_66] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_66 }, + }, + computations: { + from_num: num_66 * 100, + from_frac: frac_66 * 10.0, + list_from_num: [num_66, num_66, num_66], + }, +} + +x_67 = 3.14 +y_67 = 1.23e45 +z_67 = 0.5 + +my_str_67 : Str +my_str_67 = "one" + +binops_67 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_67 : U64 -> U64 +add_one_67 = |n| n + 1 + +map_add_one_67 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_67 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_67 = |arg_one, arg_two| arg_one * arg_two + +num_67 = 42 +frac_67 = 4.2 +str_67 = "hello" + +# Polymorphic empty collections +empty_list_67 = [] + +# Mixed polymorphic structures +mixed_67 = { + numbers: { value: num_67, list: [num_67, num_67], float: frac }, + strings: { value: str_67, list: [str_67, str_67] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_67 }, + }, + computations: { + from_num: num_67 * 100, + from_frac: frac_67 * 10.0, + list_from_num: [num_67, num_67, num_67], + }, +} + +x_68 = 3.14 +y_68 = 1.23e45 +z_68 = 0.5 + +my_str_68 : Str +my_str_68 = "one" + +binops_68 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_68 : U64 -> U64 +add_one_68 = |n| n + 1 + +map_add_one_68 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_68 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_68 = |arg_one, arg_two| arg_one * arg_two + +num_68 = 42 +frac_68 = 4.2 +str_68 = "hello" + +# Polymorphic empty collections +empty_list_68 = [] + +# Mixed polymorphic structures +mixed_68 = { + numbers: { value: num_68, list: [num_68, num_68], float: frac }, + strings: { value: str_68, list: [str_68, str_68] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_68 }, + }, + computations: { + from_num: num_68 * 100, + from_frac: frac_68 * 10.0, + list_from_num: [num_68, num_68, num_68], + }, +} + +x_69 = 3.14 +y_69 = 1.23e45 +z_69 = 0.5 + +my_str_69 : Str +my_str_69 = "one" + +binops_69 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_69 : U64 -> U64 +add_one_69 = |n| n + 1 + +map_add_one_69 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_69 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_69 = |arg_one, arg_two| arg_one * arg_two + +num_69 = 42 +frac_69 = 4.2 +str_69 = "hello" + +# Polymorphic empty collections +empty_list_69 = [] + +# Mixed polymorphic structures +mixed_69 = { + numbers: { value: num_69, list: [num_69, num_69], float: frac }, + strings: { value: str_69, list: [str_69, str_69] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_69 }, + }, + computations: { + from_num: num_69 * 100, + from_frac: frac_69 * 10.0, + list_from_num: [num_69, num_69, num_69], + }, +} + +x_70 = 3.14 +y_70 = 1.23e45 +z_70 = 0.5 + +my_str_70 : Str +my_str_70 = "one" + +binops_70 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_70 : U64 -> U64 +add_one_70 = |n| n + 1 + +map_add_one_70 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_70 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_70 = |arg_one, arg_two| arg_one * arg_two + +num_70 = 42 +frac_70 = 4.2 +str_70 = "hello" + +# Polymorphic empty collections +empty_list_70 = [] + +# Mixed polymorphic structures +mixed_70 = { + numbers: { value: num_70, list: [num_70, num_70], float: frac }, + strings: { value: str_70, list: [str_70, str_70] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_70 }, + }, + computations: { + from_num: num_70 * 100, + from_frac: frac_70 * 10.0, + list_from_num: [num_70, num_70, num_70], + }, +} + +x_71 = 3.14 +y_71 = 1.23e45 +z_71 = 0.5 + +my_str_71 : Str +my_str_71 = "one" + +binops_71 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_71 : U64 -> U64 +add_one_71 = |n| n + 1 + +map_add_one_71 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_71 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_71 = |arg_one, arg_two| arg_one * arg_two + +num_71 = 42 +frac_71 = 4.2 +str_71 = "hello" + +# Polymorphic empty collections +empty_list_71 = [] + +# Mixed polymorphic structures +mixed_71 = { + numbers: { value: num_71, list: [num_71, num_71], float: frac }, + strings: { value: str_71, list: [str_71, str_71] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_71 }, + }, + computations: { + from_num: num_71 * 100, + from_frac: frac_71 * 10.0, + list_from_num: [num_71, num_71, num_71], + }, +} + +x_72 = 3.14 +y_72 = 1.23e45 +z_72 = 0.5 + +my_str_72 : Str +my_str_72 = "one" + +binops_72 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_72 : U64 -> U64 +add_one_72 = |n| n + 1 + +map_add_one_72 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_72 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_72 = |arg_one, arg_two| arg_one * arg_two + +num_72 = 42 +frac_72 = 4.2 +str_72 = "hello" + +# Polymorphic empty collections +empty_list_72 = [] + +# Mixed polymorphic structures +mixed_72 = { + numbers: { value: num_72, list: [num_72, num_72], float: frac }, + strings: { value: str_72, list: [str_72, str_72] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_72 }, + }, + computations: { + from_num: num_72 * 100, + from_frac: frac_72 * 10.0, + list_from_num: [num_72, num_72, num_72], + }, +} + +x_73 = 3.14 +y_73 = 1.23e45 +z_73 = 0.5 + +my_str_73 : Str +my_str_73 = "one" + +binops_73 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_73 : U64 -> U64 +add_one_73 = |n| n + 1 + +map_add_one_73 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_73 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_73 = |arg_one, arg_two| arg_one * arg_two + +num_73 = 42 +frac_73 = 4.2 +str_73 = "hello" + +# Polymorphic empty collections +empty_list_73 = [] + +# Mixed polymorphic structures +mixed_73 = { + numbers: { value: num_73, list: [num_73, num_73], float: frac }, + strings: { value: str_73, list: [str_73, str_73] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_73 }, + }, + computations: { + from_num: num_73 * 100, + from_frac: frac_73 * 10.0, + list_from_num: [num_73, num_73, num_73], + }, +} + +x_74 = 3.14 +y_74 = 1.23e45 +z_74 = 0.5 + +my_str_74 : Str +my_str_74 = "one" + +binops_74 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_74 : U64 -> U64 +add_one_74 = |n| n + 1 + +map_add_one_74 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_74 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_74 = |arg_one, arg_two| arg_one * arg_two + +num_74 = 42 +frac_74 = 4.2 +str_74 = "hello" + +# Polymorphic empty collections +empty_list_74 = [] + +# Mixed polymorphic structures +mixed_74 = { + numbers: { value: num_74, list: [num_74, num_74], float: frac }, + strings: { value: str_74, list: [str_74, str_74] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_74 }, + }, + computations: { + from_num: num_74 * 100, + from_frac: frac_74 * 10.0, + list_from_num: [num_74, num_74, num_74], + }, +} + +x_75 = 3.14 +y_75 = 1.23e45 +z_75 = 0.5 + +my_str_75 : Str +my_str_75 = "one" + +binops_75 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_75 : U64 -> U64 +add_one_75 = |n| n + 1 + +map_add_one_75 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_75 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_75 = |arg_one, arg_two| arg_one * arg_two + +num_75 = 42 +frac_75 = 4.2 +str_75 = "hello" + +# Polymorphic empty collections +empty_list_75 = [] + +# Mixed polymorphic structures +mixed_75 = { + numbers: { value: num_75, list: [num_75, num_75], float: frac }, + strings: { value: str_75, list: [str_75, str_75] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_75 }, + }, + computations: { + from_num: num_75 * 100, + from_frac: frac_75 * 10.0, + list_from_num: [num_75, num_75, num_75], + }, +} + +x_76 = 3.14 +y_76 = 1.23e45 +z_76 = 0.5 + +my_str_76 : Str +my_str_76 = "one" + +binops_76 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_76 : U64 -> U64 +add_one_76 = |n| n + 1 + +map_add_one_76 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_76 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_76 = |arg_one, arg_two| arg_one * arg_two + +num_76 = 42 +frac_76 = 4.2 +str_76 = "hello" + +# Polymorphic empty collections +empty_list_76 = [] + +# Mixed polymorphic structures +mixed_76 = { + numbers: { value: num_76, list: [num_76, num_76], float: frac }, + strings: { value: str_76, list: [str_76, str_76] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_76 }, + }, + computations: { + from_num: num_76 * 100, + from_frac: frac_76 * 10.0, + list_from_num: [num_76, num_76, num_76], + }, +} + +x_77 = 3.14 +y_77 = 1.23e45 +z_77 = 0.5 + +my_str_77 : Str +my_str_77 = "one" + +binops_77 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_77 : U64 -> U64 +add_one_77 = |n| n + 1 + +map_add_one_77 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_77 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_77 = |arg_one, arg_two| arg_one * arg_two + +num_77 = 42 +frac_77 = 4.2 +str_77 = "hello" + +# Polymorphic empty collections +empty_list_77 = [] + +# Mixed polymorphic structures +mixed_77 = { + numbers: { value: num_77, list: [num_77, num_77], float: frac }, + strings: { value: str_77, list: [str_77, str_77] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_77 }, + }, + computations: { + from_num: num_77 * 100, + from_frac: frac_77 * 10.0, + list_from_num: [num_77, num_77, num_77], + }, +} + +x_78 = 3.14 +y_78 = 1.23e45 +z_78 = 0.5 + +my_str_78 : Str +my_str_78 = "one" + +binops_78 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_78 : U64 -> U64 +add_one_78 = |n| n + 1 + +map_add_one_78 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_78 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_78 = |arg_one, arg_two| arg_one * arg_two + +num_78 = 42 +frac_78 = 4.2 +str_78 = "hello" + +# Polymorphic empty collections +empty_list_78 = [] + +# Mixed polymorphic structures +mixed_78 = { + numbers: { value: num_78, list: [num_78, num_78], float: frac }, + strings: { value: str_78, list: [str_78, str_78] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_78 }, + }, + computations: { + from_num: num_78 * 100, + from_frac: frac_78 * 10.0, + list_from_num: [num_78, num_78, num_78], + }, +} + +x_79 = 3.14 +y_79 = 1.23e45 +z_79 = 0.5 + +my_str_79 : Str +my_str_79 = "one" + +binops_79 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_79 : U64 -> U64 +add_one_79 = |n| n + 1 + +map_add_one_79 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_79 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_79 = |arg_one, arg_two| arg_one * arg_two + +num_79 = 42 +frac_79 = 4.2 +str_79 = "hello" + +# Polymorphic empty collections +empty_list_79 = [] + +# Mixed polymorphic structures +mixed_79 = { + numbers: { value: num_79, list: [num_79, num_79], float: frac }, + strings: { value: str_79, list: [str_79, str_79] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_79 }, + }, + computations: { + from_num: num_79 * 100, + from_frac: frac_79 * 10.0, + list_from_num: [num_79, num_79, num_79], + }, +} + +x_80 = 3.14 +y_80 = 1.23e45 +z_80 = 0.5 + +my_str_80 : Str +my_str_80 = "one" + +binops_80 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_80 : U64 -> U64 +add_one_80 = |n| n + 1 + +map_add_one_80 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_80 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_80 = |arg_one, arg_two| arg_one * arg_two + +num_80 = 42 +frac_80 = 4.2 +str_80 = "hello" + +# Polymorphic empty collections +empty_list_80 = [] + +# Mixed polymorphic structures +mixed_80 = { + numbers: { value: num_80, list: [num_80, num_80], float: frac }, + strings: { value: str_80, list: [str_80, str_80] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_80 }, + }, + computations: { + from_num: num_80 * 100, + from_frac: frac_80 * 10.0, + list_from_num: [num_80, num_80, num_80], + }, +} + +x_81 = 3.14 +y_81 = 1.23e45 +z_81 = 0.5 + +my_str_81 : Str +my_str_81 = "one" + +binops_81 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_81 : U64 -> U64 +add_one_81 = |n| n + 1 + +map_add_one_81 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_81 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_81 = |arg_one, arg_two| arg_one * arg_two + +num_81 = 42 +frac_81 = 4.2 +str_81 = "hello" + +# Polymorphic empty collections +empty_list_81 = [] + +# Mixed polymorphic structures +mixed_81 = { + numbers: { value: num_81, list: [num_81, num_81], float: frac }, + strings: { value: str_81, list: [str_81, str_81] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_81 }, + }, + computations: { + from_num: num_81 * 100, + from_frac: frac_81 * 10.0, + list_from_num: [num_81, num_81, num_81], + }, +} + +x_82 = 3.14 +y_82 = 1.23e45 +z_82 = 0.5 + +my_str_82 : Str +my_str_82 = "one" + +binops_82 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_82 : U64 -> U64 +add_one_82 = |n| n + 1 + +map_add_one_82 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_82 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_82 = |arg_one, arg_two| arg_one * arg_two + +num_82 = 42 +frac_82 = 4.2 +str_82 = "hello" + +# Polymorphic empty collections +empty_list_82 = [] + +# Mixed polymorphic structures +mixed_82 = { + numbers: { value: num_82, list: [num_82, num_82], float: frac }, + strings: { value: str_82, list: [str_82, str_82] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_82 }, + }, + computations: { + from_num: num_82 * 100, + from_frac: frac_82 * 10.0, + list_from_num: [num_82, num_82, num_82], + }, +} + +x_83 = 3.14 +y_83 = 1.23e45 +z_83 = 0.5 + +my_str_83 : Str +my_str_83 = "one" + +binops_83 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_83 : U64 -> U64 +add_one_83 = |n| n + 1 + +map_add_one_83 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_83 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_83 = |arg_one, arg_two| arg_one * arg_two + +num_83 = 42 +frac_83 = 4.2 +str_83 = "hello" + +# Polymorphic empty collections +empty_list_83 = [] + +# Mixed polymorphic structures +mixed_83 = { + numbers: { value: num_83, list: [num_83, num_83], float: frac }, + strings: { value: str_83, list: [str_83, str_83] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_83 }, + }, + computations: { + from_num: num_83 * 100, + from_frac: frac_83 * 10.0, + list_from_num: [num_83, num_83, num_83], + }, +} + +x_84 = 3.14 +y_84 = 1.23e45 +z_84 = 0.5 + +my_str_84 : Str +my_str_84 = "one" + +binops_84 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_84 : U64 -> U64 +add_one_84 = |n| n + 1 + +map_add_one_84 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_84 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_84 = |arg_one, arg_two| arg_one * arg_two + +num_84 = 42 +frac_84 = 4.2 +str_84 = "hello" + +# Polymorphic empty collections +empty_list_84 = [] + +# Mixed polymorphic structures +mixed_84 = { + numbers: { value: num_84, list: [num_84, num_84], float: frac }, + strings: { value: str_84, list: [str_84, str_84] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_84 }, + }, + computations: { + from_num: num_84 * 100, + from_frac: frac_84 * 10.0, + list_from_num: [num_84, num_84, num_84], + }, +} + +x_85 = 3.14 +y_85 = 1.23e45 +z_85 = 0.5 + +my_str_85 : Str +my_str_85 = "one" + +binops_85 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_85 : U64 -> U64 +add_one_85 = |n| n + 1 + +map_add_one_85 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_85 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_85 = |arg_one, arg_two| arg_one * arg_two + +num_85 = 42 +frac_85 = 4.2 +str_85 = "hello" + +# Polymorphic empty collections +empty_list_85 = [] + +# Mixed polymorphic structures +mixed_85 = { + numbers: { value: num_85, list: [num_85, num_85], float: frac }, + strings: { value: str_85, list: [str_85, str_85] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_85 }, + }, + computations: { + from_num: num_85 * 100, + from_frac: frac_85 * 10.0, + list_from_num: [num_85, num_85, num_85], + }, +} + +x_86 = 3.14 +y_86 = 1.23e45 +z_86 = 0.5 + +my_str_86 : Str +my_str_86 = "one" + +binops_86 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_86 : U64 -> U64 +add_one_86 = |n| n + 1 + +map_add_one_86 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_86 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_86 = |arg_one, arg_two| arg_one * arg_two + +num_86 = 42 +frac_86 = 4.2 +str_86 = "hello" + +# Polymorphic empty collections +empty_list_86 = [] + +# Mixed polymorphic structures +mixed_86 = { + numbers: { value: num_86, list: [num_86, num_86], float: frac }, + strings: { value: str_86, list: [str_86, str_86] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_86 }, + }, + computations: { + from_num: num_86 * 100, + from_frac: frac_86 * 10.0, + list_from_num: [num_86, num_86, num_86], + }, +} + +x_87 = 3.14 +y_87 = 1.23e45 +z_87 = 0.5 + +my_str_87 : Str +my_str_87 = "one" + +binops_87 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_87 : U64 -> U64 +add_one_87 = |n| n + 1 + +map_add_one_87 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_87 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_87 = |arg_one, arg_two| arg_one * arg_two + +num_87 = 42 +frac_87 = 4.2 +str_87 = "hello" + +# Polymorphic empty collections +empty_list_87 = [] + +# Mixed polymorphic structures +mixed_87 = { + numbers: { value: num_87, list: [num_87, num_87], float: frac }, + strings: { value: str_87, list: [str_87, str_87] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_87 }, + }, + computations: { + from_num: num_87 * 100, + from_frac: frac_87 * 10.0, + list_from_num: [num_87, num_87, num_87], + }, +} + +x_88 = 3.14 +y_88 = 1.23e45 +z_88 = 0.5 + +my_str_88 : Str +my_str_88 = "one" + +binops_88 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_88 : U64 -> U64 +add_one_88 = |n| n + 1 + +map_add_one_88 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_88 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_88 = |arg_one, arg_two| arg_one * arg_two + +num_88 = 42 +frac_88 = 4.2 +str_88 = "hello" + +# Polymorphic empty collections +empty_list_88 = [] + +# Mixed polymorphic structures +mixed_88 = { + numbers: { value: num_88, list: [num_88, num_88], float: frac }, + strings: { value: str_88, list: [str_88, str_88] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_88 }, + }, + computations: { + from_num: num_88 * 100, + from_frac: frac_88 * 10.0, + list_from_num: [num_88, num_88, num_88], + }, +} + +x_89 = 3.14 +y_89 = 1.23e45 +z_89 = 0.5 + +my_str_89 : Str +my_str_89 = "one" + +binops_89 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_89 : U64 -> U64 +add_one_89 = |n| n + 1 + +map_add_one_89 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_89 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_89 = |arg_one, arg_two| arg_one * arg_two + +num_89 = 42 +frac_89 = 4.2 +str_89 = "hello" + +# Polymorphic empty collections +empty_list_89 = [] + +# Mixed polymorphic structures +mixed_89 = { + numbers: { value: num_89, list: [num_89, num_89], float: frac }, + strings: { value: str_89, list: [str_89, str_89] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_89 }, + }, + computations: { + from_num: num_89 * 100, + from_frac: frac_89 * 10.0, + list_from_num: [num_89, num_89, num_89], + }, +} + +x_90 = 3.14 +y_90 = 1.23e45 +z_90 = 0.5 + +my_str_90 : Str +my_str_90 = "one" + +binops_90 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_90 : U64 -> U64 +add_one_90 = |n| n + 1 + +map_add_one_90 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_90 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_90 = |arg_one, arg_two| arg_one * arg_two + +num_90 = 42 +frac_90 = 4.2 +str_90 = "hello" + +# Polymorphic empty collections +empty_list_90 = [] + +# Mixed polymorphic structures +mixed_90 = { + numbers: { value: num_90, list: [num_90, num_90], float: frac }, + strings: { value: str_90, list: [str_90, str_90] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_90 }, + }, + computations: { + from_num: num_90 * 100, + from_frac: frac_90 * 10.0, + list_from_num: [num_90, num_90, num_90], + }, +} + +x_91 = 3.14 +y_91 = 1.23e45 +z_91 = 0.5 + +my_str_91 : Str +my_str_91 = "one" + +binops_91 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_91 : U64 -> U64 +add_one_91 = |n| n + 1 + +map_add_one_91 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_91 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_91 = |arg_one, arg_two| arg_one * arg_two + +num_91 = 42 +frac_91 = 4.2 +str_91 = "hello" + +# Polymorphic empty collections +empty_list_91 = [] + +# Mixed polymorphic structures +mixed_91 = { + numbers: { value: num_91, list: [num_91, num_91], float: frac }, + strings: { value: str_91, list: [str_91, str_91] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_91 }, + }, + computations: { + from_num: num_91 * 100, + from_frac: frac_91 * 10.0, + list_from_num: [num_91, num_91, num_91], + }, +} + +x_92 = 3.14 +y_92 = 1.23e45 +z_92 = 0.5 + +my_str_92 : Str +my_str_92 = "one" + +binops_92 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_92 : U64 -> U64 +add_one_92 = |n| n + 1 + +map_add_one_92 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_92 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_92 = |arg_one, arg_two| arg_one * arg_two + +num_92 = 42 +frac_92 = 4.2 +str_92 = "hello" + +# Polymorphic empty collections +empty_list_92 = [] + +# Mixed polymorphic structures +mixed_92 = { + numbers: { value: num_92, list: [num_92, num_92], float: frac }, + strings: { value: str_92, list: [str_92, str_92] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_92 }, + }, + computations: { + from_num: num_92 * 100, + from_frac: frac_92 * 10.0, + list_from_num: [num_92, num_92, num_92], + }, +} + +x_93 = 3.14 +y_93 = 1.23e45 +z_93 = 0.5 + +my_str_93 : Str +my_str_93 = "one" + +binops_93 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_93 : U64 -> U64 +add_one_93 = |n| n + 1 + +map_add_one_93 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_93 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_93 = |arg_one, arg_two| arg_one * arg_two + +num_93 = 42 +frac_93 = 4.2 +str_93 = "hello" + +# Polymorphic empty collections +empty_list_93 = [] + +# Mixed polymorphic structures +mixed_93 = { + numbers: { value: num_93, list: [num_93, num_93], float: frac }, + strings: { value: str_93, list: [str_93, str_93] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_93 }, + }, + computations: { + from_num: num_93 * 100, + from_frac: frac_93 * 10.0, + list_from_num: [num_93, num_93, num_93], + }, +} + +x_94 = 3.14 +y_94 = 1.23e45 +z_94 = 0.5 + +my_str_94 : Str +my_str_94 = "one" + +binops_94 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_94 : U64 -> U64 +add_one_94 = |n| n + 1 + +map_add_one_94 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_94 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_94 = |arg_one, arg_two| arg_one * arg_two + +num_94 = 42 +frac_94 = 4.2 +str_94 = "hello" + +# Polymorphic empty collections +empty_list_94 = [] + +# Mixed polymorphic structures +mixed_94 = { + numbers: { value: num_94, list: [num_94, num_94], float: frac }, + strings: { value: str_94, list: [str_94, str_94] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_94 }, + }, + computations: { + from_num: num_94 * 100, + from_frac: frac_94 * 10.0, + list_from_num: [num_94, num_94, num_94], + }, +} + +x_95 = 3.14 +y_95 = 1.23e45 +z_95 = 0.5 + +my_str_95 : Str +my_str_95 = "one" + +binops_95 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_95 : U64 -> U64 +add_one_95 = |n| n + 1 + +map_add_one_95 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_95 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_95 = |arg_one, arg_two| arg_one * arg_two + +num_95 = 42 +frac_95 = 4.2 +str_95 = "hello" + +# Polymorphic empty collections +empty_list_95 = [] + +# Mixed polymorphic structures +mixed_95 = { + numbers: { value: num_95, list: [num_95, num_95], float: frac }, + strings: { value: str_95, list: [str_95, str_95] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_95 }, + }, + computations: { + from_num: num_95 * 100, + from_frac: frac_95 * 10.0, + list_from_num: [num_95, num_95, num_95], + }, +} + +x_96 = 3.14 +y_96 = 1.23e45 +z_96 = 0.5 + +my_str_96 : Str +my_str_96 = "one" + +binops_96 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_96 : U64 -> U64 +add_one_96 = |n| n + 1 + +map_add_one_96 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_96 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_96 = |arg_one, arg_two| arg_one * arg_two + +num_96 = 42 +frac_96 = 4.2 +str_96 = "hello" + +# Polymorphic empty collections +empty_list_96 = [] + +# Mixed polymorphic structures +mixed_96 = { + numbers: { value: num_96, list: [num_96, num_96], float: frac }, + strings: { value: str_96, list: [str_96, str_96] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_96 }, + }, + computations: { + from_num: num_96 * 100, + from_frac: frac_96 * 10.0, + list_from_num: [num_96, num_96, num_96], + }, +} + +x_97 = 3.14 +y_97 = 1.23e45 +z_97 = 0.5 + +my_str_97 : Str +my_str_97 = "one" + +binops_97 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_97 : U64 -> U64 +add_one_97 = |n| n + 1 + +map_add_one_97 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_97 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_97 = |arg_one, arg_two| arg_one * arg_two + +num_97 = 42 +frac_97 = 4.2 +str_97 = "hello" + +# Polymorphic empty collections +empty_list_97 = [] + +# Mixed polymorphic structures +mixed_97 = { + numbers: { value: num_97, list: [num_97, num_97], float: frac }, + strings: { value: str_97, list: [str_97, str_97] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_97 }, + }, + computations: { + from_num: num_97 * 100, + from_frac: frac_97 * 10.0, + list_from_num: [num_97, num_97, num_97], + }, +} + +x_98 = 3.14 +y_98 = 1.23e45 +z_98 = 0.5 + +my_str_98 : Str +my_str_98 = "one" + +binops_98 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_98 : U64 -> U64 +add_one_98 = |n| n + 1 + +map_add_one_98 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_98 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_98 = |arg_one, arg_two| arg_one * arg_two + +num_98 = 42 +frac_98 = 4.2 +str_98 = "hello" + +# Polymorphic empty collections +empty_list_98 = [] + +# Mixed polymorphic structures +mixed_98 = { + numbers: { value: num_98, list: [num_98, num_98], float: frac }, + strings: { value: str_98, list: [str_98, str_98] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_98 }, + }, + computations: { + from_num: num_98 * 100, + from_frac: frac_98 * 10.0, + list_from_num: [num_98, num_98, num_98], + }, +} + +x_99 = 3.14 +y_99 = 1.23e45 +z_99 = 0.5 + +my_str_99 : Str +my_str_99 = "one" + +binops_99 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_99 : U64 -> U64 +add_one_99 = |n| n + 1 + +map_add_one_99 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_99 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_99 = |arg_one, arg_two| arg_one * arg_two + +num_99 = 42 +frac_99 = 4.2 +str_99 = "hello" + +# Polymorphic empty collections +empty_list_99 = [] + +# Mixed polymorphic structures +mixed_99 = { + numbers: { value: num_99, list: [num_99, num_99], float: frac }, + strings: { value: str_99, list: [str_99, str_99] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_99 }, + }, + computations: { + from_num: num_99 * 100, + from_frac: frac_99 * 10.0, + list_from_num: [num_99, num_99, num_99], + }, +} + +x_100 = 3.14 +y_100 = 1.23e45 +z_100 = 0.5 + +my_str_100 : Str +my_str_100 = "one" + +binops_100 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_100 : U64 -> U64 +add_one_100 = |n| n + 1 + +map_add_one_100 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_100 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_100 = |arg_one, arg_two| arg_one * arg_two + +num_100 = 42 +frac_100 = 4.2 +str_100 = "hello" + +# Polymorphic empty collections +empty_list_100 = [] + +# Mixed polymorphic structures +mixed_100 = { + numbers: { value: num_100, list: [num_100, num_100], float: frac }, + strings: { value: str_100, list: [str_100, str_100] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_100 }, + }, + computations: { + from_num: num_100 * 100, + from_frac: frac_100 * 10.0, + list_from_num: [num_100, num_100, num_100], + }, +} + +x_101 = 3.14 +y_101 = 1.23e45 +z_101 = 0.5 + +my_str_101 : Str +my_str_101 = "one" + +binops_101 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_101 : U64 -> U64 +add_one_101 = |n| n + 1 + +map_add_one_101 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_101 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_101 = |arg_one, arg_two| arg_one * arg_two + +num_101 = 42 +frac_101 = 4.2 +str_101 = "hello" + +# Polymorphic empty collections +empty_list_101 = [] + +# Mixed polymorphic structures +mixed_101 = { + numbers: { value: num_101, list: [num_101, num_101], float: frac }, + strings: { value: str_101, list: [str_101, str_101] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_101 }, + }, + computations: { + from_num: num_101 * 100, + from_frac: frac_101 * 10.0, + list_from_num: [num_101, num_101, num_101], + }, +} + +x_102 = 3.14 +y_102 = 1.23e45 +z_102 = 0.5 + +my_str_102 : Str +my_str_102 = "one" + +binops_102 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_102 : U64 -> U64 +add_one_102 = |n| n + 1 + +map_add_one_102 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_102 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_102 = |arg_one, arg_two| arg_one * arg_two + +num_102 = 42 +frac_102 = 4.2 +str_102 = "hello" + +# Polymorphic empty collections +empty_list_102 = [] + +# Mixed polymorphic structures +mixed_102 = { + numbers: { value: num_102, list: [num_102, num_102], float: frac }, + strings: { value: str_102, list: [str_102, str_102] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_102 }, + }, + computations: { + from_num: num_102 * 100, + from_frac: frac_102 * 10.0, + list_from_num: [num_102, num_102, num_102], + }, +} + +x_103 = 3.14 +y_103 = 1.23e45 +z_103 = 0.5 + +my_str_103 : Str +my_str_103 = "one" + +binops_103 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_103 : U64 -> U64 +add_one_103 = |n| n + 1 + +map_add_one_103 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_103 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_103 = |arg_one, arg_two| arg_one * arg_two + +num_103 = 42 +frac_103 = 4.2 +str_103 = "hello" + +# Polymorphic empty collections +empty_list_103 = [] + +# Mixed polymorphic structures +mixed_103 = { + numbers: { value: num_103, list: [num_103, num_103], float: frac }, + strings: { value: str_103, list: [str_103, str_103] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_103 }, + }, + computations: { + from_num: num_103 * 100, + from_frac: frac_103 * 10.0, + list_from_num: [num_103, num_103, num_103], + }, +} + +x_104 = 3.14 +y_104 = 1.23e45 +z_104 = 0.5 + +my_str_104 : Str +my_str_104 = "one" + +binops_104 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_104 : U64 -> U64 +add_one_104 = |n| n + 1 + +map_add_one_104 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_104 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_104 = |arg_one, arg_two| arg_one * arg_two + +num_104 = 42 +frac_104 = 4.2 +str_104 = "hello" + +# Polymorphic empty collections +empty_list_104 = [] + +# Mixed polymorphic structures +mixed_104 = { + numbers: { value: num_104, list: [num_104, num_104], float: frac }, + strings: { value: str_104, list: [str_104, str_104] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_104 }, + }, + computations: { + from_num: num_104 * 100, + from_frac: frac_104 * 10.0, + list_from_num: [num_104, num_104, num_104], + }, +} + +x_105 = 3.14 +y_105 = 1.23e45 +z_105 = 0.5 + +my_str_105 : Str +my_str_105 = "one" + +binops_105 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_105 : U64 -> U64 +add_one_105 = |n| n + 1 + +map_add_one_105 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_105 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_105 = |arg_one, arg_two| arg_one * arg_two + +num_105 = 42 +frac_105 = 4.2 +str_105 = "hello" + +# Polymorphic empty collections +empty_list_105 = [] + +# Mixed polymorphic structures +mixed_105 = { + numbers: { value: num_105, list: [num_105, num_105], float: frac }, + strings: { value: str_105, list: [str_105, str_105] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_105 }, + }, + computations: { + from_num: num_105 * 100, + from_frac: frac_105 * 10.0, + list_from_num: [num_105, num_105, num_105], + }, +} + +x_106 = 3.14 +y_106 = 1.23e45 +z_106 = 0.5 + +my_str_106 : Str +my_str_106 = "one" + +binops_106 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_106 : U64 -> U64 +add_one_106 = |n| n + 1 + +map_add_one_106 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_106 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_106 = |arg_one, arg_two| arg_one * arg_two + +num_106 = 42 +frac_106 = 4.2 +str_106 = "hello" + +# Polymorphic empty collections +empty_list_106 = [] + +# Mixed polymorphic structures +mixed_106 = { + numbers: { value: num_106, list: [num_106, num_106], float: frac }, + strings: { value: str_106, list: [str_106, str_106] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_106 }, + }, + computations: { + from_num: num_106 * 100, + from_frac: frac_106 * 10.0, + list_from_num: [num_106, num_106, num_106], + }, +} + +x_107 = 3.14 +y_107 = 1.23e45 +z_107 = 0.5 + +my_str_107 : Str +my_str_107 = "one" + +binops_107 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_107 : U64 -> U64 +add_one_107 = |n| n + 1 + +map_add_one_107 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_107 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_107 = |arg_one, arg_two| arg_one * arg_two + +num_107 = 42 +frac_107 = 4.2 +str_107 = "hello" + +# Polymorphic empty collections +empty_list_107 = [] + +# Mixed polymorphic structures +mixed_107 = { + numbers: { value: num_107, list: [num_107, num_107], float: frac }, + strings: { value: str_107, list: [str_107, str_107] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_107 }, + }, + computations: { + from_num: num_107 * 100, + from_frac: frac_107 * 10.0, + list_from_num: [num_107, num_107, num_107], + }, +} + +x_108 = 3.14 +y_108 = 1.23e45 +z_108 = 0.5 + +my_str_108 : Str +my_str_108 = "one" + +binops_108 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_108 : U64 -> U64 +add_one_108 = |n| n + 1 + +map_add_one_108 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_108 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_108 = |arg_one, arg_two| arg_one * arg_two + +num_108 = 42 +frac_108 = 4.2 +str_108 = "hello" + +# Polymorphic empty collections +empty_list_108 = [] + +# Mixed polymorphic structures +mixed_108 = { + numbers: { value: num_108, list: [num_108, num_108], float: frac }, + strings: { value: str_108, list: [str_108, str_108] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_108 }, + }, + computations: { + from_num: num_108 * 100, + from_frac: frac_108 * 10.0, + list_from_num: [num_108, num_108, num_108], + }, +} + +x_109 = 3.14 +y_109 = 1.23e45 +z_109 = 0.5 + +my_str_109 : Str +my_str_109 = "one" + +binops_109 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_109 : U64 -> U64 +add_one_109 = |n| n + 1 + +map_add_one_109 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_109 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_109 = |arg_one, arg_two| arg_one * arg_two + +num_109 = 42 +frac_109 = 4.2 +str_109 = "hello" + +# Polymorphic empty collections +empty_list_109 = [] + +# Mixed polymorphic structures +mixed_109 = { + numbers: { value: num_109, list: [num_109, num_109], float: frac }, + strings: { value: str_109, list: [str_109, str_109] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_109 }, + }, + computations: { + from_num: num_109 * 100, + from_frac: frac_109 * 10.0, + list_from_num: [num_109, num_109, num_109], + }, +} + +x_110 = 3.14 +y_110 = 1.23e45 +z_110 = 0.5 + +my_str_110 : Str +my_str_110 = "one" + +binops_110 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_110 : U64 -> U64 +add_one_110 = |n| n + 1 + +map_add_one_110 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_110 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_110 = |arg_one, arg_two| arg_one * arg_two + +num_110 = 42 +frac_110 = 4.2 +str_110 = "hello" + +# Polymorphic empty collections +empty_list_110 = [] + +# Mixed polymorphic structures +mixed_110 = { + numbers: { value: num_110, list: [num_110, num_110], float: frac }, + strings: { value: str_110, list: [str_110, str_110] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_110 }, + }, + computations: { + from_num: num_110 * 100, + from_frac: frac_110 * 10.0, + list_from_num: [num_110, num_110, num_110], + }, +} + +x_111 = 3.14 +y_111 = 1.23e45 +z_111 = 0.5 + +my_str_111 : Str +my_str_111 = "one" + +binops_111 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_111 : U64 -> U64 +add_one_111 = |n| n + 1 + +map_add_one_111 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_111 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_111 = |arg_one, arg_two| arg_one * arg_two + +num_111 = 42 +frac_111 = 4.2 +str_111 = "hello" + +# Polymorphic empty collections +empty_list_111 = [] + +# Mixed polymorphic structures +mixed_111 = { + numbers: { value: num_111, list: [num_111, num_111], float: frac }, + strings: { value: str_111, list: [str_111, str_111] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_111 }, + }, + computations: { + from_num: num_111 * 100, + from_frac: frac_111 * 10.0, + list_from_num: [num_111, num_111, num_111], + }, +} + +x_112 = 3.14 +y_112 = 1.23e45 +z_112 = 0.5 + +my_str_112 : Str +my_str_112 = "one" + +binops_112 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_112 : U64 -> U64 +add_one_112 = |n| n + 1 + +map_add_one_112 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_112 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_112 = |arg_one, arg_two| arg_one * arg_two + +num_112 = 42 +frac_112 = 4.2 +str_112 = "hello" + +# Polymorphic empty collections +empty_list_112 = [] + +# Mixed polymorphic structures +mixed_112 = { + numbers: { value: num_112, list: [num_112, num_112], float: frac }, + strings: { value: str_112, list: [str_112, str_112] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_112 }, + }, + computations: { + from_num: num_112 * 100, + from_frac: frac_112 * 10.0, + list_from_num: [num_112, num_112, num_112], + }, +} + +x_113 = 3.14 +y_113 = 1.23e45 +z_113 = 0.5 + +my_str_113 : Str +my_str_113 = "one" + +binops_113 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_113 : U64 -> U64 +add_one_113 = |n| n + 1 + +map_add_one_113 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_113 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_113 = |arg_one, arg_two| arg_one * arg_two + +num_113 = 42 +frac_113 = 4.2 +str_113 = "hello" + +# Polymorphic empty collections +empty_list_113 = [] + +# Mixed polymorphic structures +mixed_113 = { + numbers: { value: num_113, list: [num_113, num_113], float: frac }, + strings: { value: str_113, list: [str_113, str_113] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_113 }, + }, + computations: { + from_num: num_113 * 100, + from_frac: frac_113 * 10.0, + list_from_num: [num_113, num_113, num_113], + }, +} + +x_114 = 3.14 +y_114 = 1.23e45 +z_114 = 0.5 + +my_str_114 : Str +my_str_114 = "one" + +binops_114 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_114 : U64 -> U64 +add_one_114 = |n| n + 1 + +map_add_one_114 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_114 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_114 = |arg_one, arg_two| arg_one * arg_two + +num_114 = 42 +frac_114 = 4.2 +str_114 = "hello" + +# Polymorphic empty collections +empty_list_114 = [] + +# Mixed polymorphic structures +mixed_114 = { + numbers: { value: num_114, list: [num_114, num_114], float: frac }, + strings: { value: str_114, list: [str_114, str_114] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_114 }, + }, + computations: { + from_num: num_114 * 100, + from_frac: frac_114 * 10.0, + list_from_num: [num_114, num_114, num_114], + }, +} + +x_115 = 3.14 +y_115 = 1.23e45 +z_115 = 0.5 + +my_str_115 : Str +my_str_115 = "one" + +binops_115 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_115 : U64 -> U64 +add_one_115 = |n| n + 1 + +map_add_one_115 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_115 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_115 = |arg_one, arg_two| arg_one * arg_two + +num_115 = 42 +frac_115 = 4.2 +str_115 = "hello" + +# Polymorphic empty collections +empty_list_115 = [] + +# Mixed polymorphic structures +mixed_115 = { + numbers: { value: num_115, list: [num_115, num_115], float: frac }, + strings: { value: str_115, list: [str_115, str_115] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_115 }, + }, + computations: { + from_num: num_115 * 100, + from_frac: frac_115 * 10.0, + list_from_num: [num_115, num_115, num_115], + }, +} + +x_116 = 3.14 +y_116 = 1.23e45 +z_116 = 0.5 + +my_str_116 : Str +my_str_116 = "one" + +binops_116 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_116 : U64 -> U64 +add_one_116 = |n| n + 1 + +map_add_one_116 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_116 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_116 = |arg_one, arg_two| arg_one * arg_two + +num_116 = 42 +frac_116 = 4.2 +str_116 = "hello" + +# Polymorphic empty collections +empty_list_116 = [] + +# Mixed polymorphic structures +mixed_116 = { + numbers: { value: num_116, list: [num_116, num_116], float: frac }, + strings: { value: str_116, list: [str_116, str_116] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_116 }, + }, + computations: { + from_num: num_116 * 100, + from_frac: frac_116 * 10.0, + list_from_num: [num_116, num_116, num_116], + }, +} + +x_117 = 3.14 +y_117 = 1.23e45 +z_117 = 0.5 + +my_str_117 : Str +my_str_117 = "one" + +binops_117 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_117 : U64 -> U64 +add_one_117 = |n| n + 1 + +map_add_one_117 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_117 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_117 = |arg_one, arg_two| arg_one * arg_two + +num_117 = 42 +frac_117 = 4.2 +str_117 = "hello" + +# Polymorphic empty collections +empty_list_117 = [] + +# Mixed polymorphic structures +mixed_117 = { + numbers: { value: num_117, list: [num_117, num_117], float: frac }, + strings: { value: str_117, list: [str_117, str_117] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_117 }, + }, + computations: { + from_num: num_117 * 100, + from_frac: frac_117 * 10.0, + list_from_num: [num_117, num_117, num_117], + }, +} + +x_118 = 3.14 +y_118 = 1.23e45 +z_118 = 0.5 + +my_str_118 : Str +my_str_118 = "one" + +binops_118 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_118 : U64 -> U64 +add_one_118 = |n| n + 1 + +map_add_one_118 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_118 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_118 = |arg_one, arg_two| arg_one * arg_two + +num_118 = 42 +frac_118 = 4.2 +str_118 = "hello" + +# Polymorphic empty collections +empty_list_118 = [] + +# Mixed polymorphic structures +mixed_118 = { + numbers: { value: num_118, list: [num_118, num_118], float: frac }, + strings: { value: str_118, list: [str_118, str_118] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_118 }, + }, + computations: { + from_num: num_118 * 100, + from_frac: frac_118 * 10.0, + list_from_num: [num_118, num_118, num_118], + }, +} + +x_119 = 3.14 +y_119 = 1.23e45 +z_119 = 0.5 + +my_str_119 : Str +my_str_119 = "one" + +binops_119 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_119 : U64 -> U64 +add_one_119 = |n| n + 1 + +map_add_one_119 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_119 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_119 = |arg_one, arg_two| arg_one * arg_two + +num_119 = 42 +frac_119 = 4.2 +str_119 = "hello" + +# Polymorphic empty collections +empty_list_119 = [] + +# Mixed polymorphic structures +mixed_119 = { + numbers: { value: num_119, list: [num_119, num_119], float: frac }, + strings: { value: str_119, list: [str_119, str_119] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_119 }, + }, + computations: { + from_num: num_119 * 100, + from_frac: frac_119 * 10.0, + list_from_num: [num_119, num_119, num_119], + }, +} + +x_120 = 3.14 +y_120 = 1.23e45 +z_120 = 0.5 + +my_str_120 : Str +my_str_120 = "one" + +binops_120 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_120 : U64 -> U64 +add_one_120 = |n| n + 1 + +map_add_one_120 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_120 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_120 = |arg_one, arg_two| arg_one * arg_two + +num_120 = 42 +frac_120 = 4.2 +str_120 = "hello" + +# Polymorphic empty collections +empty_list_120 = [] + +# Mixed polymorphic structures +mixed_120 = { + numbers: { value: num_120, list: [num_120, num_120], float: frac }, + strings: { value: str_120, list: [str_120, str_120] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_120 }, + }, + computations: { + from_num: num_120 * 100, + from_frac: frac_120 * 10.0, + list_from_num: [num_120, num_120, num_120], + }, +} + +x_121 = 3.14 +y_121 = 1.23e45 +z_121 = 0.5 + +my_str_121 : Str +my_str_121 = "one" + +binops_121 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_121 : U64 -> U64 +add_one_121 = |n| n + 1 + +map_add_one_121 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_121 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_121 = |arg_one, arg_two| arg_one * arg_two + +num_121 = 42 +frac_121 = 4.2 +str_121 = "hello" + +# Polymorphic empty collections +empty_list_121 = [] + +# Mixed polymorphic structures +mixed_121 = { + numbers: { value: num_121, list: [num_121, num_121], float: frac }, + strings: { value: str_121, list: [str_121, str_121] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_121 }, + }, + computations: { + from_num: num_121 * 100, + from_frac: frac_121 * 10.0, + list_from_num: [num_121, num_121, num_121], + }, +} + +x_122 = 3.14 +y_122 = 1.23e45 +z_122 = 0.5 + +my_str_122 : Str +my_str_122 = "one" + +binops_122 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_122 : U64 -> U64 +add_one_122 = |n| n + 1 + +map_add_one_122 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_122 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_122 = |arg_one, arg_two| arg_one * arg_two + +num_122 = 42 +frac_122 = 4.2 +str_122 = "hello" + +# Polymorphic empty collections +empty_list_122 = [] + +# Mixed polymorphic structures +mixed_122 = { + numbers: { value: num_122, list: [num_122, num_122], float: frac }, + strings: { value: str_122, list: [str_122, str_122] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_122 }, + }, + computations: { + from_num: num_122 * 100, + from_frac: frac_122 * 10.0, + list_from_num: [num_122, num_122, num_122], + }, +} + +x_123 = 3.14 +y_123 = 1.23e45 +z_123 = 0.5 + +my_str_123 : Str +my_str_123 = "one" + +binops_123 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_123 : U64 -> U64 +add_one_123 = |n| n + 1 + +map_add_one_123 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_123 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_123 = |arg_one, arg_two| arg_one * arg_two + +num_123 = 42 +frac_123 = 4.2 +str_123 = "hello" + +# Polymorphic empty collections +empty_list_123 = [] + +# Mixed polymorphic structures +mixed_123 = { + numbers: { value: num_123, list: [num_123, num_123], float: frac }, + strings: { value: str_123, list: [str_123, str_123] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_123 }, + }, + computations: { + from_num: num_123 * 100, + from_frac: frac_123 * 10.0, + list_from_num: [num_123, num_123, num_123], + }, +} + +x_124 = 3.14 +y_124 = 1.23e45 +z_124 = 0.5 + +my_str_124 : Str +my_str_124 = "one" + +binops_124 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_124 : U64 -> U64 +add_one_124 = |n| n + 1 + +map_add_one_124 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_124 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_124 = |arg_one, arg_two| arg_one * arg_two + +num_124 = 42 +frac_124 = 4.2 +str_124 = "hello" + +# Polymorphic empty collections +empty_list_124 = [] + +# Mixed polymorphic structures +mixed_124 = { + numbers: { value: num_124, list: [num_124, num_124], float: frac }, + strings: { value: str_124, list: [str_124, str_124] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_124 }, + }, + computations: { + from_num: num_124 * 100, + from_frac: frac_124 * 10.0, + list_from_num: [num_124, num_124, num_124], + }, +} + +x_125 = 3.14 +y_125 = 1.23e45 +z_125 = 0.5 + +my_str_125 : Str +my_str_125 = "one" + +binops_125 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_125 : U64 -> U64 +add_one_125 = |n| n + 1 + +map_add_one_125 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_125 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_125 = |arg_one, arg_two| arg_one * arg_two + +num_125 = 42 +frac_125 = 4.2 +str_125 = "hello" + +# Polymorphic empty collections +empty_list_125 = [] + +# Mixed polymorphic structures +mixed_125 = { + numbers: { value: num_125, list: [num_125, num_125], float: frac }, + strings: { value: str_125, list: [str_125, str_125] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_125 }, + }, + computations: { + from_num: num_125 * 100, + from_frac: frac_125 * 10.0, + list_from_num: [num_125, num_125, num_125], + }, +} + +x_126 = 3.14 +y_126 = 1.23e45 +z_126 = 0.5 + +my_str_126 : Str +my_str_126 = "one" + +binops_126 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_126 : U64 -> U64 +add_one_126 = |n| n + 1 + +map_add_one_126 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_126 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_126 = |arg_one, arg_two| arg_one * arg_two + +num_126 = 42 +frac_126 = 4.2 +str_126 = "hello" + +# Polymorphic empty collections +empty_list_126 = [] + +# Mixed polymorphic structures +mixed_126 = { + numbers: { value: num_126, list: [num_126, num_126], float: frac }, + strings: { value: str_126, list: [str_126, str_126] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_126 }, + }, + computations: { + from_num: num_126 * 100, + from_frac: frac_126 * 10.0, + list_from_num: [num_126, num_126, num_126], + }, +} + +x_127 = 3.14 +y_127 = 1.23e45 +z_127 = 0.5 + +my_str_127 : Str +my_str_127 = "one" + +binops_127 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_127 : U64 -> U64 +add_one_127 = |n| n + 1 + +map_add_one_127 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_127 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_127 = |arg_one, arg_two| arg_one * arg_two + +num_127 = 42 +frac_127 = 4.2 +str_127 = "hello" + +# Polymorphic empty collections +empty_list_127 = [] + +# Mixed polymorphic structures +mixed_127 = { + numbers: { value: num_127, list: [num_127, num_127], float: frac }, + strings: { value: str_127, list: [str_127, str_127] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_127 }, + }, + computations: { + from_num: num_127 * 100, + from_frac: frac_127 * 10.0, + list_from_num: [num_127, num_127, num_127], + }, +} + +x_128 = 3.14 +y_128 = 1.23e45 +z_128 = 0.5 + +my_str_128 : Str +my_str_128 = "one" + +binops_128 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_128 : U64 -> U64 +add_one_128 = |n| n + 1 + +map_add_one_128 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_128 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_128 = |arg_one, arg_two| arg_one * arg_two + +num_128 = 42 +frac_128 = 4.2 +str_128 = "hello" + +# Polymorphic empty collections +empty_list_128 = [] + +# Mixed polymorphic structures +mixed_128 = { + numbers: { value: num_128, list: [num_128, num_128], float: frac }, + strings: { value: str_128, list: [str_128, str_128] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_128 }, + }, + computations: { + from_num: num_128 * 100, + from_frac: frac_128 * 10.0, + list_from_num: [num_128, num_128, num_128], + }, +} + +x_129 = 3.14 +y_129 = 1.23e45 +z_129 = 0.5 + +my_str_129 : Str +my_str_129 = "one" + +binops_129 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_129 : U64 -> U64 +add_one_129 = |n| n + 1 + +map_add_one_129 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_129 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_129 = |arg_one, arg_two| arg_one * arg_two + +num_129 = 42 +frac_129 = 4.2 +str_129 = "hello" + +# Polymorphic empty collections +empty_list_129 = [] + +# Mixed polymorphic structures +mixed_129 = { + numbers: { value: num_129, list: [num_129, num_129], float: frac }, + strings: { value: str_129, list: [str_129, str_129] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_129 }, + }, + computations: { + from_num: num_129 * 100, + from_frac: frac_129 * 10.0, + list_from_num: [num_129, num_129, num_129], + }, +} + +x_130 = 3.14 +y_130 = 1.23e45 +z_130 = 0.5 + +my_str_130 : Str +my_str_130 = "one" + +binops_130 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_130 : U64 -> U64 +add_one_130 = |n| n + 1 + +map_add_one_130 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_130 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_130 = |arg_one, arg_two| arg_one * arg_two + +num_130 = 42 +frac_130 = 4.2 +str_130 = "hello" + +# Polymorphic empty collections +empty_list_130 = [] + +# Mixed polymorphic structures +mixed_130 = { + numbers: { value: num_130, list: [num_130, num_130], float: frac }, + strings: { value: str_130, list: [str_130, str_130] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_130 }, + }, + computations: { + from_num: num_130 * 100, + from_frac: frac_130 * 10.0, + list_from_num: [num_130, num_130, num_130], + }, +} + +x_131 = 3.14 +y_131 = 1.23e45 +z_131 = 0.5 + +my_str_131 : Str +my_str_131 = "one" + +binops_131 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_131 : U64 -> U64 +add_one_131 = |n| n + 1 + +map_add_one_131 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_131 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_131 = |arg_one, arg_two| arg_one * arg_two + +num_131 = 42 +frac_131 = 4.2 +str_131 = "hello" + +# Polymorphic empty collections +empty_list_131 = [] + +# Mixed polymorphic structures +mixed_131 = { + numbers: { value: num_131, list: [num_131, num_131], float: frac }, + strings: { value: str_131, list: [str_131, str_131] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_131 }, + }, + computations: { + from_num: num_131 * 100, + from_frac: frac_131 * 10.0, + list_from_num: [num_131, num_131, num_131], + }, +} + +x_132 = 3.14 +y_132 = 1.23e45 +z_132 = 0.5 + +my_str_132 : Str +my_str_132 = "one" + +binops_132 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_132 : U64 -> U64 +add_one_132 = |n| n + 1 + +map_add_one_132 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_132 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_132 = |arg_one, arg_two| arg_one * arg_two + +num_132 = 42 +frac_132 = 4.2 +str_132 = "hello" + +# Polymorphic empty collections +empty_list_132 = [] + +# Mixed polymorphic structures +mixed_132 = { + numbers: { value: num_132, list: [num_132, num_132], float: frac }, + strings: { value: str_132, list: [str_132, str_132] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_132 }, + }, + computations: { + from_num: num_132 * 100, + from_frac: frac_132 * 10.0, + list_from_num: [num_132, num_132, num_132], + }, +} + +x_133 = 3.14 +y_133 = 1.23e45 +z_133 = 0.5 + +my_str_133 : Str +my_str_133 = "one" + +binops_133 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_133 : U64 -> U64 +add_one_133 = |n| n + 1 + +map_add_one_133 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_133 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_133 = |arg_one, arg_two| arg_one * arg_two + +num_133 = 42 +frac_133 = 4.2 +str_133 = "hello" + +# Polymorphic empty collections +empty_list_133 = [] + +# Mixed polymorphic structures +mixed_133 = { + numbers: { value: num_133, list: [num_133, num_133], float: frac }, + strings: { value: str_133, list: [str_133, str_133] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_133 }, + }, + computations: { + from_num: num_133 * 100, + from_frac: frac_133 * 10.0, + list_from_num: [num_133, num_133, num_133], + }, +} + +x_134 = 3.14 +y_134 = 1.23e45 +z_134 = 0.5 + +my_str_134 : Str +my_str_134 = "one" + +binops_134 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_134 : U64 -> U64 +add_one_134 = |n| n + 1 + +map_add_one_134 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_134 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_134 = |arg_one, arg_two| arg_one * arg_two + +num_134 = 42 +frac_134 = 4.2 +str_134 = "hello" + +# Polymorphic empty collections +empty_list_134 = [] + +# Mixed polymorphic structures +mixed_134 = { + numbers: { value: num_134, list: [num_134, num_134], float: frac }, + strings: { value: str_134, list: [str_134, str_134] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_134 }, + }, + computations: { + from_num: num_134 * 100, + from_frac: frac_134 * 10.0, + list_from_num: [num_134, num_134, num_134], + }, +} + +x_135 = 3.14 +y_135 = 1.23e45 +z_135 = 0.5 + +my_str_135 : Str +my_str_135 = "one" + +binops_135 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_135 : U64 -> U64 +add_one_135 = |n| n + 1 + +map_add_one_135 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_135 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_135 = |arg_one, arg_two| arg_one * arg_two + +num_135 = 42 +frac_135 = 4.2 +str_135 = "hello" + +# Polymorphic empty collections +empty_list_135 = [] + +# Mixed polymorphic structures +mixed_135 = { + numbers: { value: num_135, list: [num_135, num_135], float: frac }, + strings: { value: str_135, list: [str_135, str_135] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_135 }, + }, + computations: { + from_num: num_135 * 100, + from_frac: frac_135 * 10.0, + list_from_num: [num_135, num_135, num_135], + }, +} + +x_136 = 3.14 +y_136 = 1.23e45 +z_136 = 0.5 + +my_str_136 : Str +my_str_136 = "one" + +binops_136 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_136 : U64 -> U64 +add_one_136 = |n| n + 1 + +map_add_one_136 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_136 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_136 = |arg_one, arg_two| arg_one * arg_two + +num_136 = 42 +frac_136 = 4.2 +str_136 = "hello" + +# Polymorphic empty collections +empty_list_136 = [] + +# Mixed polymorphic structures +mixed_136 = { + numbers: { value: num_136, list: [num_136, num_136], float: frac }, + strings: { value: str_136, list: [str_136, str_136] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_136 }, + }, + computations: { + from_num: num_136 * 100, + from_frac: frac_136 * 10.0, + list_from_num: [num_136, num_136, num_136], + }, +} + +x_137 = 3.14 +y_137 = 1.23e45 +z_137 = 0.5 + +my_str_137 : Str +my_str_137 = "one" + +binops_137 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_137 : U64 -> U64 +add_one_137 = |n| n + 1 + +map_add_one_137 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_137 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_137 = |arg_one, arg_two| arg_one * arg_two + +num_137 = 42 +frac_137 = 4.2 +str_137 = "hello" + +# Polymorphic empty collections +empty_list_137 = [] + +# Mixed polymorphic structures +mixed_137 = { + numbers: { value: num_137, list: [num_137, num_137], float: frac }, + strings: { value: str_137, list: [str_137, str_137] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_137 }, + }, + computations: { + from_num: num_137 * 100, + from_frac: frac_137 * 10.0, + list_from_num: [num_137, num_137, num_137], + }, +} + +x_138 = 3.14 +y_138 = 1.23e45 +z_138 = 0.5 + +my_str_138 : Str +my_str_138 = "one" + +binops_138 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_138 : U64 -> U64 +add_one_138 = |n| n + 1 + +map_add_one_138 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_138 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_138 = |arg_one, arg_two| arg_one * arg_two + +num_138 = 42 +frac_138 = 4.2 +str_138 = "hello" + +# Polymorphic empty collections +empty_list_138 = [] + +# Mixed polymorphic structures +mixed_138 = { + numbers: { value: num_138, list: [num_138, num_138], float: frac }, + strings: { value: str_138, list: [str_138, str_138] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_138 }, + }, + computations: { + from_num: num_138 * 100, + from_frac: frac_138 * 10.0, + list_from_num: [num_138, num_138, num_138], + }, +} + +x_139 = 3.14 +y_139 = 1.23e45 +z_139 = 0.5 + +my_str_139 : Str +my_str_139 = "one" + +binops_139 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_139 : U64 -> U64 +add_one_139 = |n| n + 1 + +map_add_one_139 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_139 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_139 = |arg_one, arg_two| arg_one * arg_two + +num_139 = 42 +frac_139 = 4.2 +str_139 = "hello" + +# Polymorphic empty collections +empty_list_139 = [] + +# Mixed polymorphic structures +mixed_139 = { + numbers: { value: num_139, list: [num_139, num_139], float: frac }, + strings: { value: str_139, list: [str_139, str_139] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_139 }, + }, + computations: { + from_num: num_139 * 100, + from_frac: frac_139 * 10.0, + list_from_num: [num_139, num_139, num_139], + }, +} + +x_140 = 3.14 +y_140 = 1.23e45 +z_140 = 0.5 + +my_str_140 : Str +my_str_140 = "one" + +binops_140 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_140 : U64 -> U64 +add_one_140 = |n| n + 1 + +map_add_one_140 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_140 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_140 = |arg_one, arg_two| arg_one * arg_two + +num_140 = 42 +frac_140 = 4.2 +str_140 = "hello" + +# Polymorphic empty collections +empty_list_140 = [] + +# Mixed polymorphic structures +mixed_140 = { + numbers: { value: num_140, list: [num_140, num_140], float: frac }, + strings: { value: str_140, list: [str_140, str_140] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_140 }, + }, + computations: { + from_num: num_140 * 100, + from_frac: frac_140 * 10.0, + list_from_num: [num_140, num_140, num_140], + }, +} + +x_141 = 3.14 +y_141 = 1.23e45 +z_141 = 0.5 + +my_str_141 : Str +my_str_141 = "one" + +binops_141 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_141 : U64 -> U64 +add_one_141 = |n| n + 1 + +map_add_one_141 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_141 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_141 = |arg_one, arg_two| arg_one * arg_two + +num_141 = 42 +frac_141 = 4.2 +str_141 = "hello" + +# Polymorphic empty collections +empty_list_141 = [] + +# Mixed polymorphic structures +mixed_141 = { + numbers: { value: num_141, list: [num_141, num_141], float: frac }, + strings: { value: str_141, list: [str_141, str_141] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_141 }, + }, + computations: { + from_num: num_141 * 100, + from_frac: frac_141 * 10.0, + list_from_num: [num_141, num_141, num_141], + }, +} + +x_142 = 3.14 +y_142 = 1.23e45 +z_142 = 0.5 + +my_str_142 : Str +my_str_142 = "one" + +binops_142 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_142 : U64 -> U64 +add_one_142 = |n| n + 1 + +map_add_one_142 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_142 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_142 = |arg_one, arg_two| arg_one * arg_two + +num_142 = 42 +frac_142 = 4.2 +str_142 = "hello" + +# Polymorphic empty collections +empty_list_142 = [] + +# Mixed polymorphic structures +mixed_142 = { + numbers: { value: num_142, list: [num_142, num_142], float: frac }, + strings: { value: str_142, list: [str_142, str_142] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_142 }, + }, + computations: { + from_num: num_142 * 100, + from_frac: frac_142 * 10.0, + list_from_num: [num_142, num_142, num_142], + }, +} + +x_143 = 3.14 +y_143 = 1.23e45 +z_143 = 0.5 + +my_str_143 : Str +my_str_143 = "one" + +binops_143 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_143 : U64 -> U64 +add_one_143 = |n| n + 1 + +map_add_one_143 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_143 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_143 = |arg_one, arg_two| arg_one * arg_two + +num_143 = 42 +frac_143 = 4.2 +str_143 = "hello" + +# Polymorphic empty collections +empty_list_143 = [] + +# Mixed polymorphic structures +mixed_143 = { + numbers: { value: num_143, list: [num_143, num_143], float: frac }, + strings: { value: str_143, list: [str_143, str_143] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_143 }, + }, + computations: { + from_num: num_143 * 100, + from_frac: frac_143 * 10.0, + list_from_num: [num_143, num_143, num_143], + }, +} + +x_144 = 3.14 +y_144 = 1.23e45 +z_144 = 0.5 + +my_str_144 : Str +my_str_144 = "one" + +binops_144 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_144 : U64 -> U64 +add_one_144 = |n| n + 1 + +map_add_one_144 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_144 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_144 = |arg_one, arg_two| arg_one * arg_two + +num_144 = 42 +frac_144 = 4.2 +str_144 = "hello" + +# Polymorphic empty collections +empty_list_144 = [] + +# Mixed polymorphic structures +mixed_144 = { + numbers: { value: num_144, list: [num_144, num_144], float: frac }, + strings: { value: str_144, list: [str_144, str_144] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_144 }, + }, + computations: { + from_num: num_144 * 100, + from_frac: frac_144 * 10.0, + list_from_num: [num_144, num_144, num_144], + }, +} + +x_145 = 3.14 +y_145 = 1.23e45 +z_145 = 0.5 + +my_str_145 : Str +my_str_145 = "one" + +binops_145 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_145 : U64 -> U64 +add_one_145 = |n| n + 1 + +map_add_one_145 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_145 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_145 = |arg_one, arg_two| arg_one * arg_two + +num_145 = 42 +frac_145 = 4.2 +str_145 = "hello" + +# Polymorphic empty collections +empty_list_145 = [] + +# Mixed polymorphic structures +mixed_145 = { + numbers: { value: num_145, list: [num_145, num_145], float: frac }, + strings: { value: str_145, list: [str_145, str_145] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_145 }, + }, + computations: { + from_num: num_145 * 100, + from_frac: frac_145 * 10.0, + list_from_num: [num_145, num_145, num_145], + }, +} + +x_146 = 3.14 +y_146 = 1.23e45 +z_146 = 0.5 + +my_str_146 : Str +my_str_146 = "one" + +binops_146 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_146 : U64 -> U64 +add_one_146 = |n| n + 1 + +map_add_one_146 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_146 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_146 = |arg_one, arg_two| arg_one * arg_two + +num_146 = 42 +frac_146 = 4.2 +str_146 = "hello" + +# Polymorphic empty collections +empty_list_146 = [] + +# Mixed polymorphic structures +mixed_146 = { + numbers: { value: num_146, list: [num_146, num_146], float: frac }, + strings: { value: str_146, list: [str_146, str_146] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_146 }, + }, + computations: { + from_num: num_146 * 100, + from_frac: frac_146 * 10.0, + list_from_num: [num_146, num_146, num_146], + }, +} + +x_147 = 3.14 +y_147 = 1.23e45 +z_147 = 0.5 + +my_str_147 : Str +my_str_147 = "one" + +binops_147 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_147 : U64 -> U64 +add_one_147 = |n| n + 1 + +map_add_one_147 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_147 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_147 = |arg_one, arg_two| arg_one * arg_two + +num_147 = 42 +frac_147 = 4.2 +str_147 = "hello" + +# Polymorphic empty collections +empty_list_147 = [] + +# Mixed polymorphic structures +mixed_147 = { + numbers: { value: num_147, list: [num_147, num_147], float: frac }, + strings: { value: str_147, list: [str_147, str_147] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_147 }, + }, + computations: { + from_num: num_147 * 100, + from_frac: frac_147 * 10.0, + list_from_num: [num_147, num_147, num_147], + }, +} + +x_148 = 3.14 +y_148 = 1.23e45 +z_148 = 0.5 + +my_str_148 : Str +my_str_148 = "one" + +binops_148 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_148 : U64 -> U64 +add_one_148 = |n| n + 1 + +map_add_one_148 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_148 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_148 = |arg_one, arg_two| arg_one * arg_two + +num_148 = 42 +frac_148 = 4.2 +str_148 = "hello" + +# Polymorphic empty collections +empty_list_148 = [] + +# Mixed polymorphic structures +mixed_148 = { + numbers: { value: num_148, list: [num_148, num_148], float: frac }, + strings: { value: str_148, list: [str_148, str_148] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_148 }, + }, + computations: { + from_num: num_148 * 100, + from_frac: frac_148 * 10.0, + list_from_num: [num_148, num_148, num_148], + }, +} + +x_149 = 3.14 +y_149 = 1.23e45 +z_149 = 0.5 + +my_str_149 : Str +my_str_149 = "one" + +binops_149 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_149 : U64 -> U64 +add_one_149 = |n| n + 1 + +map_add_one_149 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_149 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_149 = |arg_one, arg_two| arg_one * arg_two + +num_149 = 42 +frac_149 = 4.2 +str_149 = "hello" + +# Polymorphic empty collections +empty_list_149 = [] + +# Mixed polymorphic structures +mixed_149 = { + numbers: { value: num_149, list: [num_149, num_149], float: frac }, + strings: { value: str_149, list: [str_149, str_149] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_149 }, + }, + computations: { + from_num: num_149 * 100, + from_frac: frac_149 * 10.0, + list_from_num: [num_149, num_149, num_149], + }, +} + +x_150 = 3.14 +y_150 = 1.23e45 +z_150 = 0.5 + +my_str_150 : Str +my_str_150 = "one" + +binops_150 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_150 : U64 -> U64 +add_one_150 = |n| n + 1 + +map_add_one_150 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_150 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_150 = |arg_one, arg_two| arg_one * arg_two + +num_150 = 42 +frac_150 = 4.2 +str_150 = "hello" + +# Polymorphic empty collections +empty_list_150 = [] + +# Mixed polymorphic structures +mixed_150 = { + numbers: { value: num_150, list: [num_150, num_150], float: frac }, + strings: { value: str_150, list: [str_150, str_150] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_150 }, + }, + computations: { + from_num: num_150 * 100, + from_frac: frac_150 * 10.0, + list_from_num: [num_150, num_150, num_150], + }, +} + +x_151 = 3.14 +y_151 = 1.23e45 +z_151 = 0.5 + +my_str_151 : Str +my_str_151 = "one" + +binops_151 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_151 : U64 -> U64 +add_one_151 = |n| n + 1 + +map_add_one_151 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_151 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_151 = |arg_one, arg_two| arg_one * arg_two + +num_151 = 42 +frac_151 = 4.2 +str_151 = "hello" + +# Polymorphic empty collections +empty_list_151 = [] + +# Mixed polymorphic structures +mixed_151 = { + numbers: { value: num_151, list: [num_151, num_151], float: frac }, + strings: { value: str_151, list: [str_151, str_151] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_151 }, + }, + computations: { + from_num: num_151 * 100, + from_frac: frac_151 * 10.0, + list_from_num: [num_151, num_151, num_151], + }, +} + +x_152 = 3.14 +y_152 = 1.23e45 +z_152 = 0.5 + +my_str_152 : Str +my_str_152 = "one" + +binops_152 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_152 : U64 -> U64 +add_one_152 = |n| n + 1 + +map_add_one_152 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_152 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_152 = |arg_one, arg_two| arg_one * arg_two + +num_152 = 42 +frac_152 = 4.2 +str_152 = "hello" + +# Polymorphic empty collections +empty_list_152 = [] + +# Mixed polymorphic structures +mixed_152 = { + numbers: { value: num_152, list: [num_152, num_152], float: frac }, + strings: { value: str_152, list: [str_152, str_152] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_152 }, + }, + computations: { + from_num: num_152 * 100, + from_frac: frac_152 * 10.0, + list_from_num: [num_152, num_152, num_152], + }, +} + +x_153 = 3.14 +y_153 = 1.23e45 +z_153 = 0.5 + +my_str_153 : Str +my_str_153 = "one" + +binops_153 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_153 : U64 -> U64 +add_one_153 = |n| n + 1 + +map_add_one_153 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_153 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_153 = |arg_one, arg_two| arg_one * arg_two + +num_153 = 42 +frac_153 = 4.2 +str_153 = "hello" + +# Polymorphic empty collections +empty_list_153 = [] + +# Mixed polymorphic structures +mixed_153 = { + numbers: { value: num_153, list: [num_153, num_153], float: frac }, + strings: { value: str_153, list: [str_153, str_153] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_153 }, + }, + computations: { + from_num: num_153 * 100, + from_frac: frac_153 * 10.0, + list_from_num: [num_153, num_153, num_153], + }, +} + +x_154 = 3.14 +y_154 = 1.23e45 +z_154 = 0.5 + +my_str_154 : Str +my_str_154 = "one" + +binops_154 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_154 : U64 -> U64 +add_one_154 = |n| n + 1 + +map_add_one_154 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_154 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_154 = |arg_one, arg_two| arg_one * arg_two + +num_154 = 42 +frac_154 = 4.2 +str_154 = "hello" + +# Polymorphic empty collections +empty_list_154 = [] + +# Mixed polymorphic structures +mixed_154 = { + numbers: { value: num_154, list: [num_154, num_154], float: frac }, + strings: { value: str_154, list: [str_154, str_154] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_154 }, + }, + computations: { + from_num: num_154 * 100, + from_frac: frac_154 * 10.0, + list_from_num: [num_154, num_154, num_154], + }, +} + +x_155 = 3.14 +y_155 = 1.23e45 +z_155 = 0.5 + +my_str_155 : Str +my_str_155 = "one" + +binops_155 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_155 : U64 -> U64 +add_one_155 = |n| n + 1 + +map_add_one_155 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_155 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_155 = |arg_one, arg_two| arg_one * arg_two + +num_155 = 42 +frac_155 = 4.2 +str_155 = "hello" + +# Polymorphic empty collections +empty_list_155 = [] + +# Mixed polymorphic structures +mixed_155 = { + numbers: { value: num_155, list: [num_155, num_155], float: frac }, + strings: { value: str_155, list: [str_155, str_155] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_155 }, + }, + computations: { + from_num: num_155 * 100, + from_frac: frac_155 * 10.0, + list_from_num: [num_155, num_155, num_155], + }, +} + +x_156 = 3.14 +y_156 = 1.23e45 +z_156 = 0.5 + +my_str_156 : Str +my_str_156 = "one" + +binops_156 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_156 : U64 -> U64 +add_one_156 = |n| n + 1 + +map_add_one_156 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_156 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_156 = |arg_one, arg_two| arg_one * arg_two + +num_156 = 42 +frac_156 = 4.2 +str_156 = "hello" + +# Polymorphic empty collections +empty_list_156 = [] + +# Mixed polymorphic structures +mixed_156 = { + numbers: { value: num_156, list: [num_156, num_156], float: frac }, + strings: { value: str_156, list: [str_156, str_156] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_156 }, + }, + computations: { + from_num: num_156 * 100, + from_frac: frac_156 * 10.0, + list_from_num: [num_156, num_156, num_156], + }, +} + +x_157 = 3.14 +y_157 = 1.23e45 +z_157 = 0.5 + +my_str_157 : Str +my_str_157 = "one" + +binops_157 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_157 : U64 -> U64 +add_one_157 = |n| n + 1 + +map_add_one_157 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_157 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_157 = |arg_one, arg_two| arg_one * arg_two + +num_157 = 42 +frac_157 = 4.2 +str_157 = "hello" + +# Polymorphic empty collections +empty_list_157 = [] + +# Mixed polymorphic structures +mixed_157 = { + numbers: { value: num_157, list: [num_157, num_157], float: frac }, + strings: { value: str_157, list: [str_157, str_157] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_157 }, + }, + computations: { + from_num: num_157 * 100, + from_frac: frac_157 * 10.0, + list_from_num: [num_157, num_157, num_157], + }, +} + +x_158 = 3.14 +y_158 = 1.23e45 +z_158 = 0.5 + +my_str_158 : Str +my_str_158 = "one" + +binops_158 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_158 : U64 -> U64 +add_one_158 = |n| n + 1 + +map_add_one_158 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_158 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_158 = |arg_one, arg_two| arg_one * arg_two + +num_158 = 42 +frac_158 = 4.2 +str_158 = "hello" + +# Polymorphic empty collections +empty_list_158 = [] + +# Mixed polymorphic structures +mixed_158 = { + numbers: { value: num_158, list: [num_158, num_158], float: frac }, + strings: { value: str_158, list: [str_158, str_158] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_158 }, + }, + computations: { + from_num: num_158 * 100, + from_frac: frac_158 * 10.0, + list_from_num: [num_158, num_158, num_158], + }, +} + +x_159 = 3.14 +y_159 = 1.23e45 +z_159 = 0.5 + +my_str_159 : Str +my_str_159 = "one" + +binops_159 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_159 : U64 -> U64 +add_one_159 = |n| n + 1 + +map_add_one_159 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_159 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_159 = |arg_one, arg_two| arg_one * arg_two + +num_159 = 42 +frac_159 = 4.2 +str_159 = "hello" + +# Polymorphic empty collections +empty_list_159 = [] + +# Mixed polymorphic structures +mixed_159 = { + numbers: { value: num_159, list: [num_159, num_159], float: frac }, + strings: { value: str_159, list: [str_159, str_159] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_159 }, + }, + computations: { + from_num: num_159 * 100, + from_frac: frac_159 * 10.0, + list_from_num: [num_159, num_159, num_159], + }, +} + +x_160 = 3.14 +y_160 = 1.23e45 +z_160 = 0.5 + +my_str_160 : Str +my_str_160 = "one" + +binops_160 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_160 : U64 -> U64 +add_one_160 = |n| n + 1 + +map_add_one_160 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_160 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_160 = |arg_one, arg_two| arg_one * arg_two + +num_160 = 42 +frac_160 = 4.2 +str_160 = "hello" + +# Polymorphic empty collections +empty_list_160 = [] + +# Mixed polymorphic structures +mixed_160 = { + numbers: { value: num_160, list: [num_160, num_160], float: frac }, + strings: { value: str_160, list: [str_160, str_160] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_160 }, + }, + computations: { + from_num: num_160 * 100, + from_frac: frac_160 * 10.0, + list_from_num: [num_160, num_160, num_160], + }, +} + +x_161 = 3.14 +y_161 = 1.23e45 +z_161 = 0.5 + +my_str_161 : Str +my_str_161 = "one" + +binops_161 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_161 : U64 -> U64 +add_one_161 = |n| n + 1 + +map_add_one_161 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_161 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_161 = |arg_one, arg_two| arg_one * arg_two + +num_161 = 42 +frac_161 = 4.2 +str_161 = "hello" + +# Polymorphic empty collections +empty_list_161 = [] + +# Mixed polymorphic structures +mixed_161 = { + numbers: { value: num_161, list: [num_161, num_161], float: frac }, + strings: { value: str_161, list: [str_161, str_161] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_161 }, + }, + computations: { + from_num: num_161 * 100, + from_frac: frac_161 * 10.0, + list_from_num: [num_161, num_161, num_161], + }, +} + +x_162 = 3.14 +y_162 = 1.23e45 +z_162 = 0.5 + +my_str_162 : Str +my_str_162 = "one" + +binops_162 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_162 : U64 -> U64 +add_one_162 = |n| n + 1 + +map_add_one_162 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_162 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_162 = |arg_one, arg_two| arg_one * arg_two + +num_162 = 42 +frac_162 = 4.2 +str_162 = "hello" + +# Polymorphic empty collections +empty_list_162 = [] + +# Mixed polymorphic structures +mixed_162 = { + numbers: { value: num_162, list: [num_162, num_162], float: frac }, + strings: { value: str_162, list: [str_162, str_162] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_162 }, + }, + computations: { + from_num: num_162 * 100, + from_frac: frac_162 * 10.0, + list_from_num: [num_162, num_162, num_162], + }, +} + +x_163 = 3.14 +y_163 = 1.23e45 +z_163 = 0.5 + +my_str_163 : Str +my_str_163 = "one" + +binops_163 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_163 : U64 -> U64 +add_one_163 = |n| n + 1 + +map_add_one_163 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_163 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_163 = |arg_one, arg_two| arg_one * arg_two + +num_163 = 42 +frac_163 = 4.2 +str_163 = "hello" + +# Polymorphic empty collections +empty_list_163 = [] + +# Mixed polymorphic structures +mixed_163 = { + numbers: { value: num_163, list: [num_163, num_163], float: frac }, + strings: { value: str_163, list: [str_163, str_163] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_163 }, + }, + computations: { + from_num: num_163 * 100, + from_frac: frac_163 * 10.0, + list_from_num: [num_163, num_163, num_163], + }, +} + +x_164 = 3.14 +y_164 = 1.23e45 +z_164 = 0.5 + +my_str_164 : Str +my_str_164 = "one" + +binops_164 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_164 : U64 -> U64 +add_one_164 = |n| n + 1 + +map_add_one_164 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_164 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_164 = |arg_one, arg_two| arg_one * arg_two + +num_164 = 42 +frac_164 = 4.2 +str_164 = "hello" + +# Polymorphic empty collections +empty_list_164 = [] + +# Mixed polymorphic structures +mixed_164 = { + numbers: { value: num_164, list: [num_164, num_164], float: frac }, + strings: { value: str_164, list: [str_164, str_164] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_164 }, + }, + computations: { + from_num: num_164 * 100, + from_frac: frac_164 * 10.0, + list_from_num: [num_164, num_164, num_164], + }, +} + +x_165 = 3.14 +y_165 = 1.23e45 +z_165 = 0.5 + +my_str_165 : Str +my_str_165 = "one" + +binops_165 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_165 : U64 -> U64 +add_one_165 = |n| n + 1 + +map_add_one_165 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_165 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_165 = |arg_one, arg_two| arg_one * arg_two + +num_165 = 42 +frac_165 = 4.2 +str_165 = "hello" + +# Polymorphic empty collections +empty_list_165 = [] + +# Mixed polymorphic structures +mixed_165 = { + numbers: { value: num_165, list: [num_165, num_165], float: frac }, + strings: { value: str_165, list: [str_165, str_165] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_165 }, + }, + computations: { + from_num: num_165 * 100, + from_frac: frac_165 * 10.0, + list_from_num: [num_165, num_165, num_165], + }, +} + +x_166 = 3.14 +y_166 = 1.23e45 +z_166 = 0.5 + +my_str_166 : Str +my_str_166 = "one" + +binops_166 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_166 : U64 -> U64 +add_one_166 = |n| n + 1 + +map_add_one_166 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_166 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_166 = |arg_one, arg_two| arg_one * arg_two + +num_166 = 42 +frac_166 = 4.2 +str_166 = "hello" + +# Polymorphic empty collections +empty_list_166 = [] + +# Mixed polymorphic structures +mixed_166 = { + numbers: { value: num_166, list: [num_166, num_166], float: frac }, + strings: { value: str_166, list: [str_166, str_166] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_166 }, + }, + computations: { + from_num: num_166 * 100, + from_frac: frac_166 * 10.0, + list_from_num: [num_166, num_166, num_166], + }, +} + +x_167 = 3.14 +y_167 = 1.23e45 +z_167 = 0.5 + +my_str_167 : Str +my_str_167 = "one" + +binops_167 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_167 : U64 -> U64 +add_one_167 = |n| n + 1 + +map_add_one_167 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_167 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_167 = |arg_one, arg_two| arg_one * arg_two + +num_167 = 42 +frac_167 = 4.2 +str_167 = "hello" + +# Polymorphic empty collections +empty_list_167 = [] + +# Mixed polymorphic structures +mixed_167 = { + numbers: { value: num_167, list: [num_167, num_167], float: frac }, + strings: { value: str_167, list: [str_167, str_167] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_167 }, + }, + computations: { + from_num: num_167 * 100, + from_frac: frac_167 * 10.0, + list_from_num: [num_167, num_167, num_167], + }, +} + +x_168 = 3.14 +y_168 = 1.23e45 +z_168 = 0.5 + +my_str_168 : Str +my_str_168 = "one" + +binops_168 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_168 : U64 -> U64 +add_one_168 = |n| n + 1 + +map_add_one_168 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_168 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_168 = |arg_one, arg_two| arg_one * arg_two + +num_168 = 42 +frac_168 = 4.2 +str_168 = "hello" + +# Polymorphic empty collections +empty_list_168 = [] + +# Mixed polymorphic structures +mixed_168 = { + numbers: { value: num_168, list: [num_168, num_168], float: frac }, + strings: { value: str_168, list: [str_168, str_168] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_168 }, + }, + computations: { + from_num: num_168 * 100, + from_frac: frac_168 * 10.0, + list_from_num: [num_168, num_168, num_168], + }, +} + +x_169 = 3.14 +y_169 = 1.23e45 +z_169 = 0.5 + +my_str_169 : Str +my_str_169 = "one" + +binops_169 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_169 : U64 -> U64 +add_one_169 = |n| n + 1 + +map_add_one_169 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_169 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_169 = |arg_one, arg_two| arg_one * arg_two + +num_169 = 42 +frac_169 = 4.2 +str_169 = "hello" + +# Polymorphic empty collections +empty_list_169 = [] + +# Mixed polymorphic structures +mixed_169 = { + numbers: { value: num_169, list: [num_169, num_169], float: frac }, + strings: { value: str_169, list: [str_169, str_169] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_169 }, + }, + computations: { + from_num: num_169 * 100, + from_frac: frac_169 * 10.0, + list_from_num: [num_169, num_169, num_169], + }, +} + +x_170 = 3.14 +y_170 = 1.23e45 +z_170 = 0.5 + +my_str_170 : Str +my_str_170 = "one" + +binops_170 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_170 : U64 -> U64 +add_one_170 = |n| n + 1 + +map_add_one_170 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_170 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_170 = |arg_one, arg_two| arg_one * arg_two + +num_170 = 42 +frac_170 = 4.2 +str_170 = "hello" + +# Polymorphic empty collections +empty_list_170 = [] + +# Mixed polymorphic structures +mixed_170 = { + numbers: { value: num_170, list: [num_170, num_170], float: frac }, + strings: { value: str_170, list: [str_170, str_170] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_170 }, + }, + computations: { + from_num: num_170 * 100, + from_frac: frac_170 * 10.0, + list_from_num: [num_170, num_170, num_170], + }, +} + +x_171 = 3.14 +y_171 = 1.23e45 +z_171 = 0.5 + +my_str_171 : Str +my_str_171 = "one" + +binops_171 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_171 : U64 -> U64 +add_one_171 = |n| n + 1 + +map_add_one_171 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_171 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_171 = |arg_one, arg_two| arg_one * arg_two + +num_171 = 42 +frac_171 = 4.2 +str_171 = "hello" + +# Polymorphic empty collections +empty_list_171 = [] + +# Mixed polymorphic structures +mixed_171 = { + numbers: { value: num_171, list: [num_171, num_171], float: frac }, + strings: { value: str_171, list: [str_171, str_171] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_171 }, + }, + computations: { + from_num: num_171 * 100, + from_frac: frac_171 * 10.0, + list_from_num: [num_171, num_171, num_171], + }, +} + +x_172 = 3.14 +y_172 = 1.23e45 +z_172 = 0.5 + +my_str_172 : Str +my_str_172 = "one" + +binops_172 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_172 : U64 -> U64 +add_one_172 = |n| n + 1 + +map_add_one_172 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_172 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_172 = |arg_one, arg_two| arg_one * arg_two + +num_172 = 42 +frac_172 = 4.2 +str_172 = "hello" + +# Polymorphic empty collections +empty_list_172 = [] + +# Mixed polymorphic structures +mixed_172 = { + numbers: { value: num_172, list: [num_172, num_172], float: frac }, + strings: { value: str_172, list: [str_172, str_172] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_172 }, + }, + computations: { + from_num: num_172 * 100, + from_frac: frac_172 * 10.0, + list_from_num: [num_172, num_172, num_172], + }, +} + +x_173 = 3.14 +y_173 = 1.23e45 +z_173 = 0.5 + +my_str_173 : Str +my_str_173 = "one" + +binops_173 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_173 : U64 -> U64 +add_one_173 = |n| n + 1 + +map_add_one_173 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_173 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_173 = |arg_one, arg_two| arg_one * arg_two + +num_173 = 42 +frac_173 = 4.2 +str_173 = "hello" + +# Polymorphic empty collections +empty_list_173 = [] + +# Mixed polymorphic structures +mixed_173 = { + numbers: { value: num_173, list: [num_173, num_173], float: frac }, + strings: { value: str_173, list: [str_173, str_173] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_173 }, + }, + computations: { + from_num: num_173 * 100, + from_frac: frac_173 * 10.0, + list_from_num: [num_173, num_173, num_173], + }, +} + +x_174 = 3.14 +y_174 = 1.23e45 +z_174 = 0.5 + +my_str_174 : Str +my_str_174 = "one" + +binops_174 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_174 : U64 -> U64 +add_one_174 = |n| n + 1 + +map_add_one_174 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_174 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_174 = |arg_one, arg_two| arg_one * arg_two + +num_174 = 42 +frac_174 = 4.2 +str_174 = "hello" + +# Polymorphic empty collections +empty_list_174 = [] + +# Mixed polymorphic structures +mixed_174 = { + numbers: { value: num_174, list: [num_174, num_174], float: frac }, + strings: { value: str_174, list: [str_174, str_174] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_174 }, + }, + computations: { + from_num: num_174 * 100, + from_frac: frac_174 * 10.0, + list_from_num: [num_174, num_174, num_174], + }, +} + +x_175 = 3.14 +y_175 = 1.23e45 +z_175 = 0.5 + +my_str_175 : Str +my_str_175 = "one" + +binops_175 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_175 : U64 -> U64 +add_one_175 = |n| n + 1 + +map_add_one_175 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_175 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_175 = |arg_one, arg_two| arg_one * arg_two + +num_175 = 42 +frac_175 = 4.2 +str_175 = "hello" + +# Polymorphic empty collections +empty_list_175 = [] + +# Mixed polymorphic structures +mixed_175 = { + numbers: { value: num_175, list: [num_175, num_175], float: frac }, + strings: { value: str_175, list: [str_175, str_175] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_175 }, + }, + computations: { + from_num: num_175 * 100, + from_frac: frac_175 * 10.0, + list_from_num: [num_175, num_175, num_175], + }, +} + +x_176 = 3.14 +y_176 = 1.23e45 +z_176 = 0.5 + +my_str_176 : Str +my_str_176 = "one" + +binops_176 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_176 : U64 -> U64 +add_one_176 = |n| n + 1 + +map_add_one_176 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_176 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_176 = |arg_one, arg_two| arg_one * arg_two + +num_176 = 42 +frac_176 = 4.2 +str_176 = "hello" + +# Polymorphic empty collections +empty_list_176 = [] + +# Mixed polymorphic structures +mixed_176 = { + numbers: { value: num_176, list: [num_176, num_176], float: frac }, + strings: { value: str_176, list: [str_176, str_176] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_176 }, + }, + computations: { + from_num: num_176 * 100, + from_frac: frac_176 * 10.0, + list_from_num: [num_176, num_176, num_176], + }, +} + +x_177 = 3.14 +y_177 = 1.23e45 +z_177 = 0.5 + +my_str_177 : Str +my_str_177 = "one" + +binops_177 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_177 : U64 -> U64 +add_one_177 = |n| n + 1 + +map_add_one_177 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_177 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_177 = |arg_one, arg_two| arg_one * arg_two + +num_177 = 42 +frac_177 = 4.2 +str_177 = "hello" + +# Polymorphic empty collections +empty_list_177 = [] + +# Mixed polymorphic structures +mixed_177 = { + numbers: { value: num_177, list: [num_177, num_177], float: frac }, + strings: { value: str_177, list: [str_177, str_177] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_177 }, + }, + computations: { + from_num: num_177 * 100, + from_frac: frac_177 * 10.0, + list_from_num: [num_177, num_177, num_177], + }, +} + +x_178 = 3.14 +y_178 = 1.23e45 +z_178 = 0.5 + +my_str_178 : Str +my_str_178 = "one" + +binops_178 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_178 : U64 -> U64 +add_one_178 = |n| n + 1 + +map_add_one_178 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_178 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_178 = |arg_one, arg_two| arg_one * arg_two + +num_178 = 42 +frac_178 = 4.2 +str_178 = "hello" + +# Polymorphic empty collections +empty_list_178 = [] + +# Mixed polymorphic structures +mixed_178 = { + numbers: { value: num_178, list: [num_178, num_178], float: frac }, + strings: { value: str_178, list: [str_178, str_178] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_178 }, + }, + computations: { + from_num: num_178 * 100, + from_frac: frac_178 * 10.0, + list_from_num: [num_178, num_178, num_178], + }, +} + +x_179 = 3.14 +y_179 = 1.23e45 +z_179 = 0.5 + +my_str_179 : Str +my_str_179 = "one" + +binops_179 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_179 : U64 -> U64 +add_one_179 = |n| n + 1 + +map_add_one_179 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_179 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_179 = |arg_one, arg_two| arg_one * arg_two + +num_179 = 42 +frac_179 = 4.2 +str_179 = "hello" + +# Polymorphic empty collections +empty_list_179 = [] + +# Mixed polymorphic structures +mixed_179 = { + numbers: { value: num_179, list: [num_179, num_179], float: frac }, + strings: { value: str_179, list: [str_179, str_179] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_179 }, + }, + computations: { + from_num: num_179 * 100, + from_frac: frac_179 * 10.0, + list_from_num: [num_179, num_179, num_179], + }, +} + +x_180 = 3.14 +y_180 = 1.23e45 +z_180 = 0.5 + +my_str_180 : Str +my_str_180 = "one" + +binops_180 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_180 : U64 -> U64 +add_one_180 = |n| n + 1 + +map_add_one_180 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_180 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_180 = |arg_one, arg_two| arg_one * arg_two + +num_180 = 42 +frac_180 = 4.2 +str_180 = "hello" + +# Polymorphic empty collections +empty_list_180 = [] + +# Mixed polymorphic structures +mixed_180 = { + numbers: { value: num_180, list: [num_180, num_180], float: frac }, + strings: { value: str_180, list: [str_180, str_180] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_180 }, + }, + computations: { + from_num: num_180 * 100, + from_frac: frac_180 * 10.0, + list_from_num: [num_180, num_180, num_180], + }, +} + +x_181 = 3.14 +y_181 = 1.23e45 +z_181 = 0.5 + +my_str_181 : Str +my_str_181 = "one" + +binops_181 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_181 : U64 -> U64 +add_one_181 = |n| n + 1 + +map_add_one_181 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_181 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_181 = |arg_one, arg_two| arg_one * arg_two + +num_181 = 42 +frac_181 = 4.2 +str_181 = "hello" + +# Polymorphic empty collections +empty_list_181 = [] + +# Mixed polymorphic structures +mixed_181 = { + numbers: { value: num_181, list: [num_181, num_181], float: frac }, + strings: { value: str_181, list: [str_181, str_181] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_181 }, + }, + computations: { + from_num: num_181 * 100, + from_frac: frac_181 * 10.0, + list_from_num: [num_181, num_181, num_181], + }, +} + +x_182 = 3.14 +y_182 = 1.23e45 +z_182 = 0.5 + +my_str_182 : Str +my_str_182 = "one" + +binops_182 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_182 : U64 -> U64 +add_one_182 = |n| n + 1 + +map_add_one_182 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_182 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_182 = |arg_one, arg_two| arg_one * arg_two + +num_182 = 42 +frac_182 = 4.2 +str_182 = "hello" + +# Polymorphic empty collections +empty_list_182 = [] + +# Mixed polymorphic structures +mixed_182 = { + numbers: { value: num_182, list: [num_182, num_182], float: frac }, + strings: { value: str_182, list: [str_182, str_182] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_182 }, + }, + computations: { + from_num: num_182 * 100, + from_frac: frac_182 * 10.0, + list_from_num: [num_182, num_182, num_182], + }, +} + +x_183 = 3.14 +y_183 = 1.23e45 +z_183 = 0.5 + +my_str_183 : Str +my_str_183 = "one" + +binops_183 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_183 : U64 -> U64 +add_one_183 = |n| n + 1 + +map_add_one_183 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_183 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_183 = |arg_one, arg_two| arg_one * arg_two + +num_183 = 42 +frac_183 = 4.2 +str_183 = "hello" + +# Polymorphic empty collections +empty_list_183 = [] + +# Mixed polymorphic structures +mixed_183 = { + numbers: { value: num_183, list: [num_183, num_183], float: frac }, + strings: { value: str_183, list: [str_183, str_183] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_183 }, + }, + computations: { + from_num: num_183 * 100, + from_frac: frac_183 * 10.0, + list_from_num: [num_183, num_183, num_183], + }, +} + +x_184 = 3.14 +y_184 = 1.23e45 +z_184 = 0.5 + +my_str_184 : Str +my_str_184 = "one" + +binops_184 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_184 : U64 -> U64 +add_one_184 = |n| n + 1 + +map_add_one_184 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_184 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_184 = |arg_one, arg_two| arg_one * arg_two + +num_184 = 42 +frac_184 = 4.2 +str_184 = "hello" + +# Polymorphic empty collections +empty_list_184 = [] + +# Mixed polymorphic structures +mixed_184 = { + numbers: { value: num_184, list: [num_184, num_184], float: frac }, + strings: { value: str_184, list: [str_184, str_184] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_184 }, + }, + computations: { + from_num: num_184 * 100, + from_frac: frac_184 * 10.0, + list_from_num: [num_184, num_184, num_184], + }, +} + +x_185 = 3.14 +y_185 = 1.23e45 +z_185 = 0.5 + +my_str_185 : Str +my_str_185 = "one" + +binops_185 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_185 : U64 -> U64 +add_one_185 = |n| n + 1 + +map_add_one_185 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_185 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_185 = |arg_one, arg_two| arg_one * arg_two + +num_185 = 42 +frac_185 = 4.2 +str_185 = "hello" + +# Polymorphic empty collections +empty_list_185 = [] + +# Mixed polymorphic structures +mixed_185 = { + numbers: { value: num_185, list: [num_185, num_185], float: frac }, + strings: { value: str_185, list: [str_185, str_185] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_185 }, + }, + computations: { + from_num: num_185 * 100, + from_frac: frac_185 * 10.0, + list_from_num: [num_185, num_185, num_185], + }, +} + +x_186 = 3.14 +y_186 = 1.23e45 +z_186 = 0.5 + +my_str_186 : Str +my_str_186 = "one" + +binops_186 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_186 : U64 -> U64 +add_one_186 = |n| n + 1 + +map_add_one_186 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_186 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_186 = |arg_one, arg_two| arg_one * arg_two + +num_186 = 42 +frac_186 = 4.2 +str_186 = "hello" + +# Polymorphic empty collections +empty_list_186 = [] + +# Mixed polymorphic structures +mixed_186 = { + numbers: { value: num_186, list: [num_186, num_186], float: frac }, + strings: { value: str_186, list: [str_186, str_186] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_186 }, + }, + computations: { + from_num: num_186 * 100, + from_frac: frac_186 * 10.0, + list_from_num: [num_186, num_186, num_186], + }, +} + +x_187 = 3.14 +y_187 = 1.23e45 +z_187 = 0.5 + +my_str_187 : Str +my_str_187 = "one" + +binops_187 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_187 : U64 -> U64 +add_one_187 = |n| n + 1 + +map_add_one_187 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_187 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_187 = |arg_one, arg_two| arg_one * arg_two + +num_187 = 42 +frac_187 = 4.2 +str_187 = "hello" + +# Polymorphic empty collections +empty_list_187 = [] + +# Mixed polymorphic structures +mixed_187 = { + numbers: { value: num_187, list: [num_187, num_187], float: frac }, + strings: { value: str_187, list: [str_187, str_187] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_187 }, + }, + computations: { + from_num: num_187 * 100, + from_frac: frac_187 * 10.0, + list_from_num: [num_187, num_187, num_187], + }, +} + +x_188 = 3.14 +y_188 = 1.23e45 +z_188 = 0.5 + +my_str_188 : Str +my_str_188 = "one" + +binops_188 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_188 : U64 -> U64 +add_one_188 = |n| n + 1 + +map_add_one_188 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_188 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_188 = |arg_one, arg_two| arg_one * arg_two + +num_188 = 42 +frac_188 = 4.2 +str_188 = "hello" + +# Polymorphic empty collections +empty_list_188 = [] + +# Mixed polymorphic structures +mixed_188 = { + numbers: { value: num_188, list: [num_188, num_188], float: frac }, + strings: { value: str_188, list: [str_188, str_188] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_188 }, + }, + computations: { + from_num: num_188 * 100, + from_frac: frac_188 * 10.0, + list_from_num: [num_188, num_188, num_188], + }, +} + +x_189 = 3.14 +y_189 = 1.23e45 +z_189 = 0.5 + +my_str_189 : Str +my_str_189 = "one" + +binops_189 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_189 : U64 -> U64 +add_one_189 = |n| n + 1 + +map_add_one_189 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_189 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_189 = |arg_one, arg_two| arg_one * arg_two + +num_189 = 42 +frac_189 = 4.2 +str_189 = "hello" + +# Polymorphic empty collections +empty_list_189 = [] + +# Mixed polymorphic structures +mixed_189 = { + numbers: { value: num_189, list: [num_189, num_189], float: frac }, + strings: { value: str_189, list: [str_189, str_189] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_189 }, + }, + computations: { + from_num: num_189 * 100, + from_frac: frac_189 * 10.0, + list_from_num: [num_189, num_189, num_189], + }, +} + +x_190 = 3.14 +y_190 = 1.23e45 +z_190 = 0.5 + +my_str_190 : Str +my_str_190 = "one" + +binops_190 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_190 : U64 -> U64 +add_one_190 = |n| n + 1 + +map_add_one_190 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_190 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_190 = |arg_one, arg_two| arg_one * arg_two + +num_190 = 42 +frac_190 = 4.2 +str_190 = "hello" + +# Polymorphic empty collections +empty_list_190 = [] + +# Mixed polymorphic structures +mixed_190 = { + numbers: { value: num_190, list: [num_190, num_190], float: frac }, + strings: { value: str_190, list: [str_190, str_190] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_190 }, + }, + computations: { + from_num: num_190 * 100, + from_frac: frac_190 * 10.0, + list_from_num: [num_190, num_190, num_190], + }, +} + +x_191 = 3.14 +y_191 = 1.23e45 +z_191 = 0.5 + +my_str_191 : Str +my_str_191 = "one" + +binops_191 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_191 : U64 -> U64 +add_one_191 = |n| n + 1 + +map_add_one_191 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_191 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_191 = |arg_one, arg_two| arg_one * arg_two + +num_191 = 42 +frac_191 = 4.2 +str_191 = "hello" + +# Polymorphic empty collections +empty_list_191 = [] + +# Mixed polymorphic structures +mixed_191 = { + numbers: { value: num_191, list: [num_191, num_191], float: frac }, + strings: { value: str_191, list: [str_191, str_191] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_191 }, + }, + computations: { + from_num: num_191 * 100, + from_frac: frac_191 * 10.0, + list_from_num: [num_191, num_191, num_191], + }, +} + +x_192 = 3.14 +y_192 = 1.23e45 +z_192 = 0.5 + +my_str_192 : Str +my_str_192 = "one" + +binops_192 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_192 : U64 -> U64 +add_one_192 = |n| n + 1 + +map_add_one_192 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_192 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_192 = |arg_one, arg_two| arg_one * arg_two + +num_192 = 42 +frac_192 = 4.2 +str_192 = "hello" + +# Polymorphic empty collections +empty_list_192 = [] + +# Mixed polymorphic structures +mixed_192 = { + numbers: { value: num_192, list: [num_192, num_192], float: frac }, + strings: { value: str_192, list: [str_192, str_192] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_192 }, + }, + computations: { + from_num: num_192 * 100, + from_frac: frac_192 * 10.0, + list_from_num: [num_192, num_192, num_192], + }, +} + +x_193 = 3.14 +y_193 = 1.23e45 +z_193 = 0.5 + +my_str_193 : Str +my_str_193 = "one" + +binops_193 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_193 : U64 -> U64 +add_one_193 = |n| n + 1 + +map_add_one_193 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_193 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_193 = |arg_one, arg_two| arg_one * arg_two + +num_193 = 42 +frac_193 = 4.2 +str_193 = "hello" + +# Polymorphic empty collections +empty_list_193 = [] + +# Mixed polymorphic structures +mixed_193 = { + numbers: { value: num_193, list: [num_193, num_193], float: frac }, + strings: { value: str_193, list: [str_193, str_193] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_193 }, + }, + computations: { + from_num: num_193 * 100, + from_frac: frac_193 * 10.0, + list_from_num: [num_193, num_193, num_193], + }, +} + +x_194 = 3.14 +y_194 = 1.23e45 +z_194 = 0.5 + +my_str_194 : Str +my_str_194 = "one" + +binops_194 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_194 : U64 -> U64 +add_one_194 = |n| n + 1 + +map_add_one_194 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_194 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_194 = |arg_one, arg_two| arg_one * arg_two + +num_194 = 42 +frac_194 = 4.2 +str_194 = "hello" + +# Polymorphic empty collections +empty_list_194 = [] + +# Mixed polymorphic structures +mixed_194 = { + numbers: { value: num_194, list: [num_194, num_194], float: frac }, + strings: { value: str_194, list: [str_194, str_194] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_194 }, + }, + computations: { + from_num: num_194 * 100, + from_frac: frac_194 * 10.0, + list_from_num: [num_194, num_194, num_194], + }, +} + +x_195 = 3.14 +y_195 = 1.23e45 +z_195 = 0.5 + +my_str_195 : Str +my_str_195 = "one" + +binops_195 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_195 : U64 -> U64 +add_one_195 = |n| n + 1 + +map_add_one_195 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_195 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_195 = |arg_one, arg_two| arg_one * arg_two + +num_195 = 42 +frac_195 = 4.2 +str_195 = "hello" + +# Polymorphic empty collections +empty_list_195 = [] + +# Mixed polymorphic structures +mixed_195 = { + numbers: { value: num_195, list: [num_195, num_195], float: frac }, + strings: { value: str_195, list: [str_195, str_195] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_195 }, + }, + computations: { + from_num: num_195 * 100, + from_frac: frac_195 * 10.0, + list_from_num: [num_195, num_195, num_195], + }, +} + +x_196 = 3.14 +y_196 = 1.23e45 +z_196 = 0.5 + +my_str_196 : Str +my_str_196 = "one" + +binops_196 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_196 : U64 -> U64 +add_one_196 = |n| n + 1 + +map_add_one_196 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_196 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_196 = |arg_one, arg_two| arg_one * arg_two + +num_196 = 42 +frac_196 = 4.2 +str_196 = "hello" + +# Polymorphic empty collections +empty_list_196 = [] + +# Mixed polymorphic structures +mixed_196 = { + numbers: { value: num_196, list: [num_196, num_196], float: frac }, + strings: { value: str_196, list: [str_196, str_196] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_196 }, + }, + computations: { + from_num: num_196 * 100, + from_frac: frac_196 * 10.0, + list_from_num: [num_196, num_196, num_196], + }, +} + +x_197 = 3.14 +y_197 = 1.23e45 +z_197 = 0.5 + +my_str_197 : Str +my_str_197 = "one" + +binops_197 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_197 : U64 -> U64 +add_one_197 = |n| n + 1 + +map_add_one_197 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_197 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_197 = |arg_one, arg_two| arg_one * arg_two + +num_197 = 42 +frac_197 = 4.2 +str_197 = "hello" + +# Polymorphic empty collections +empty_list_197 = [] + +# Mixed polymorphic structures +mixed_197 = { + numbers: { value: num_197, list: [num_197, num_197], float: frac }, + strings: { value: str_197, list: [str_197, str_197] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_197 }, + }, + computations: { + from_num: num_197 * 100, + from_frac: frac_197 * 10.0, + list_from_num: [num_197, num_197, num_197], + }, +} + +x_198 = 3.14 +y_198 = 1.23e45 +z_198 = 0.5 + +my_str_198 : Str +my_str_198 = "one" + +binops_198 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_198 : U64 -> U64 +add_one_198 = |n| n + 1 + +map_add_one_198 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_198 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_198 = |arg_one, arg_two| arg_one * arg_two + +num_198 = 42 +frac_198 = 4.2 +str_198 = "hello" + +# Polymorphic empty collections +empty_list_198 = [] + +# Mixed polymorphic structures +mixed_198 = { + numbers: { value: num_198, list: [num_198, num_198], float: frac }, + strings: { value: str_198, list: [str_198, str_198] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_198 }, + }, + computations: { + from_num: num_198 * 100, + from_frac: frac_198 * 10.0, + list_from_num: [num_198, num_198, num_198], + }, +} + +x_199 = 3.14 +y_199 = 1.23e45 +z_199 = 0.5 + +my_str_199 : Str +my_str_199 = "one" + +binops_199 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_199 : U64 -> U64 +add_one_199 = |n| n + 1 + +map_add_one_199 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_199 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_199 = |arg_one, arg_two| arg_one * arg_two + +num_199 = 42 +frac_199 = 4.2 +str_199 = "hello" + +# Polymorphic empty collections +empty_list_199 = [] + +# Mixed polymorphic structures +mixed_199 = { + numbers: { value: num_199, list: [num_199, num_199], float: frac }, + strings: { value: str_199, list: [str_199, str_199] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_199 }, + }, + computations: { + from_num: num_199 * 100, + from_frac: frac_199 * 10.0, + list_from_num: [num_199, num_199, num_199], + }, +} + +x_200 = 3.14 +y_200 = 1.23e45 +z_200 = 0.5 + +my_str_200 : Str +my_str_200 = "one" + +binops_200 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_200 : U64 -> U64 +add_one_200 = |n| n + 1 + +map_add_one_200 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_200 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_200 = |arg_one, arg_two| arg_one * arg_two + +num_200 = 42 +frac_200 = 4.2 +str_200 = "hello" + +# Polymorphic empty collections +empty_list_200 = [] + +# Mixed polymorphic structures +mixed_200 = { + numbers: { value: num_200, list: [num_200, num_200], float: frac }, + strings: { value: str_200, list: [str_200, str_200] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_200 }, + }, + computations: { + from_num: num_200 * 100, + from_frac: frac_200 * 10.0, + list_from_num: [num_200, num_200, num_200], + }, +} + +x_201 = 3.14 +y_201 = 1.23e45 +z_201 = 0.5 + +my_str_201 : Str +my_str_201 = "one" + +binops_201 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_201 : U64 -> U64 +add_one_201 = |n| n + 1 + +map_add_one_201 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_201 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_201 = |arg_one, arg_two| arg_one * arg_two + +num_201 = 42 +frac_201 = 4.2 +str_201 = "hello" + +# Polymorphic empty collections +empty_list_201 = [] + +# Mixed polymorphic structures +mixed_201 = { + numbers: { value: num_201, list: [num_201, num_201], float: frac }, + strings: { value: str_201, list: [str_201, str_201] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_201 }, + }, + computations: { + from_num: num_201 * 100, + from_frac: frac_201 * 10.0, + list_from_num: [num_201, num_201, num_201], + }, +} + +x_202 = 3.14 +y_202 = 1.23e45 +z_202 = 0.5 + +my_str_202 : Str +my_str_202 = "one" + +binops_202 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_202 : U64 -> U64 +add_one_202 = |n| n + 1 + +map_add_one_202 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_202 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_202 = |arg_one, arg_two| arg_one * arg_two + +num_202 = 42 +frac_202 = 4.2 +str_202 = "hello" + +# Polymorphic empty collections +empty_list_202 = [] + +# Mixed polymorphic structures +mixed_202 = { + numbers: { value: num_202, list: [num_202, num_202], float: frac }, + strings: { value: str_202, list: [str_202, str_202] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_202 }, + }, + computations: { + from_num: num_202 * 100, + from_frac: frac_202 * 10.0, + list_from_num: [num_202, num_202, num_202], + }, +} + +x_203 = 3.14 +y_203 = 1.23e45 +z_203 = 0.5 + +my_str_203 : Str +my_str_203 = "one" + +binops_203 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_203 : U64 -> U64 +add_one_203 = |n| n + 1 + +map_add_one_203 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_203 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_203 = |arg_one, arg_two| arg_one * arg_two + +num_203 = 42 +frac_203 = 4.2 +str_203 = "hello" + +# Polymorphic empty collections +empty_list_203 = [] + +# Mixed polymorphic structures +mixed_203 = { + numbers: { value: num_203, list: [num_203, num_203], float: frac }, + strings: { value: str_203, list: [str_203, str_203] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_203 }, + }, + computations: { + from_num: num_203 * 100, + from_frac: frac_203 * 10.0, + list_from_num: [num_203, num_203, num_203], + }, +} + +x_204 = 3.14 +y_204 = 1.23e45 +z_204 = 0.5 + +my_str_204 : Str +my_str_204 = "one" + +binops_204 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_204 : U64 -> U64 +add_one_204 = |n| n + 1 + +map_add_one_204 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_204 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_204 = |arg_one, arg_two| arg_one * arg_two + +num_204 = 42 +frac_204 = 4.2 +str_204 = "hello" + +# Polymorphic empty collections +empty_list_204 = [] + +# Mixed polymorphic structures +mixed_204 = { + numbers: { value: num_204, list: [num_204, num_204], float: frac }, + strings: { value: str_204, list: [str_204, str_204] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_204 }, + }, + computations: { + from_num: num_204 * 100, + from_frac: frac_204 * 10.0, + list_from_num: [num_204, num_204, num_204], + }, +} + +x_205 = 3.14 +y_205 = 1.23e45 +z_205 = 0.5 + +my_str_205 : Str +my_str_205 = "one" + +binops_205 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_205 : U64 -> U64 +add_one_205 = |n| n + 1 + +map_add_one_205 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_205 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_205 = |arg_one, arg_two| arg_one * arg_two + +num_205 = 42 +frac_205 = 4.2 +str_205 = "hello" + +# Polymorphic empty collections +empty_list_205 = [] + +# Mixed polymorphic structures +mixed_205 = { + numbers: { value: num_205, list: [num_205, num_205], float: frac }, + strings: { value: str_205, list: [str_205, str_205] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_205 }, + }, + computations: { + from_num: num_205 * 100, + from_frac: frac_205 * 10.0, + list_from_num: [num_205, num_205, num_205], + }, +} + +x_206 = 3.14 +y_206 = 1.23e45 +z_206 = 0.5 + +my_str_206 : Str +my_str_206 = "one" + +binops_206 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_206 : U64 -> U64 +add_one_206 = |n| n + 1 + +map_add_one_206 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_206 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_206 = |arg_one, arg_two| arg_one * arg_two + +num_206 = 42 +frac_206 = 4.2 +str_206 = "hello" + +# Polymorphic empty collections +empty_list_206 = [] + +# Mixed polymorphic structures +mixed_206 = { + numbers: { value: num_206, list: [num_206, num_206], float: frac }, + strings: { value: str_206, list: [str_206, str_206] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_206 }, + }, + computations: { + from_num: num_206 * 100, + from_frac: frac_206 * 10.0, + list_from_num: [num_206, num_206, num_206], + }, +} + +x_207 = 3.14 +y_207 = 1.23e45 +z_207 = 0.5 + +my_str_207 : Str +my_str_207 = "one" + +binops_207 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_207 : U64 -> U64 +add_one_207 = |n| n + 1 + +map_add_one_207 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_207 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_207 = |arg_one, arg_two| arg_one * arg_two + +num_207 = 42 +frac_207 = 4.2 +str_207 = "hello" + +# Polymorphic empty collections +empty_list_207 = [] + +# Mixed polymorphic structures +mixed_207 = { + numbers: { value: num_207, list: [num_207, num_207], float: frac }, + strings: { value: str_207, list: [str_207, str_207] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_207 }, + }, + computations: { + from_num: num_207 * 100, + from_frac: frac_207 * 10.0, + list_from_num: [num_207, num_207, num_207], + }, +} + +x_208 = 3.14 +y_208 = 1.23e45 +z_208 = 0.5 + +my_str_208 : Str +my_str_208 = "one" + +binops_208 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_208 : U64 -> U64 +add_one_208 = |n| n + 1 + +map_add_one_208 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_208 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_208 = |arg_one, arg_two| arg_one * arg_two + +num_208 = 42 +frac_208 = 4.2 +str_208 = "hello" + +# Polymorphic empty collections +empty_list_208 = [] + +# Mixed polymorphic structures +mixed_208 = { + numbers: { value: num_208, list: [num_208, num_208], float: frac }, + strings: { value: str_208, list: [str_208, str_208] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_208 }, + }, + computations: { + from_num: num_208 * 100, + from_frac: frac_208 * 10.0, + list_from_num: [num_208, num_208, num_208], + }, +} + +x_209 = 3.14 +y_209 = 1.23e45 +z_209 = 0.5 + +my_str_209 : Str +my_str_209 = "one" + +binops_209 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_209 : U64 -> U64 +add_one_209 = |n| n + 1 + +map_add_one_209 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_209 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_209 = |arg_one, arg_two| arg_one * arg_two + +num_209 = 42 +frac_209 = 4.2 +str_209 = "hello" + +# Polymorphic empty collections +empty_list_209 = [] + +# Mixed polymorphic structures +mixed_209 = { + numbers: { value: num_209, list: [num_209, num_209], float: frac }, + strings: { value: str_209, list: [str_209, str_209] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_209 }, + }, + computations: { + from_num: num_209 * 100, + from_frac: frac_209 * 10.0, + list_from_num: [num_209, num_209, num_209], + }, +} + +x_210 = 3.14 +y_210 = 1.23e45 +z_210 = 0.5 + +my_str_210 : Str +my_str_210 = "one" + +binops_210 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_210 : U64 -> U64 +add_one_210 = |n| n + 1 + +map_add_one_210 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_210 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_210 = |arg_one, arg_two| arg_one * arg_two + +num_210 = 42 +frac_210 = 4.2 +str_210 = "hello" + +# Polymorphic empty collections +empty_list_210 = [] + +# Mixed polymorphic structures +mixed_210 = { + numbers: { value: num_210, list: [num_210, num_210], float: frac }, + strings: { value: str_210, list: [str_210, str_210] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_210 }, + }, + computations: { + from_num: num_210 * 100, + from_frac: frac_210 * 10.0, + list_from_num: [num_210, num_210, num_210], + }, +} + +x_211 = 3.14 +y_211 = 1.23e45 +z_211 = 0.5 + +my_str_211 : Str +my_str_211 = "one" + +binops_211 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_211 : U64 -> U64 +add_one_211 = |n| n + 1 + +map_add_one_211 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_211 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_211 = |arg_one, arg_two| arg_one * arg_two + +num_211 = 42 +frac_211 = 4.2 +str_211 = "hello" + +# Polymorphic empty collections +empty_list_211 = [] + +# Mixed polymorphic structures +mixed_211 = { + numbers: { value: num_211, list: [num_211, num_211], float: frac }, + strings: { value: str_211, list: [str_211, str_211] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_211 }, + }, + computations: { + from_num: num_211 * 100, + from_frac: frac_211 * 10.0, + list_from_num: [num_211, num_211, num_211], + }, +} + +x_212 = 3.14 +y_212 = 1.23e45 +z_212 = 0.5 + +my_str_212 : Str +my_str_212 = "one" + +binops_212 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_212 : U64 -> U64 +add_one_212 = |n| n + 1 + +map_add_one_212 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_212 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_212 = |arg_one, arg_two| arg_one * arg_two + +num_212 = 42 +frac_212 = 4.2 +str_212 = "hello" + +# Polymorphic empty collections +empty_list_212 = [] + +# Mixed polymorphic structures +mixed_212 = { + numbers: { value: num_212, list: [num_212, num_212], float: frac }, + strings: { value: str_212, list: [str_212, str_212] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_212 }, + }, + computations: { + from_num: num_212 * 100, + from_frac: frac_212 * 10.0, + list_from_num: [num_212, num_212, num_212], + }, +} + +x_213 = 3.14 +y_213 = 1.23e45 +z_213 = 0.5 + +my_str_213 : Str +my_str_213 = "one" + +binops_213 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_213 : U64 -> U64 +add_one_213 = |n| n + 1 + +map_add_one_213 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_213 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_213 = |arg_one, arg_two| arg_one * arg_two + +num_213 = 42 +frac_213 = 4.2 +str_213 = "hello" + +# Polymorphic empty collections +empty_list_213 = [] + +# Mixed polymorphic structures +mixed_213 = { + numbers: { value: num_213, list: [num_213, num_213], float: frac }, + strings: { value: str_213, list: [str_213, str_213] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_213 }, + }, + computations: { + from_num: num_213 * 100, + from_frac: frac_213 * 10.0, + list_from_num: [num_213, num_213, num_213], + }, +} + +x_214 = 3.14 +y_214 = 1.23e45 +z_214 = 0.5 + +my_str_214 : Str +my_str_214 = "one" + +binops_214 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_214 : U64 -> U64 +add_one_214 = |n| n + 1 + +map_add_one_214 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_214 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_214 = |arg_one, arg_two| arg_one * arg_two + +num_214 = 42 +frac_214 = 4.2 +str_214 = "hello" + +# Polymorphic empty collections +empty_list_214 = [] + +# Mixed polymorphic structures +mixed_214 = { + numbers: { value: num_214, list: [num_214, num_214], float: frac }, + strings: { value: str_214, list: [str_214, str_214] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_214 }, + }, + computations: { + from_num: num_214 * 100, + from_frac: frac_214 * 10.0, + list_from_num: [num_214, num_214, num_214], + }, +} + +x_215 = 3.14 +y_215 = 1.23e45 +z_215 = 0.5 + +my_str_215 : Str +my_str_215 = "one" + +binops_215 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_215 : U64 -> U64 +add_one_215 = |n| n + 1 + +map_add_one_215 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_215 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_215 = |arg_one, arg_two| arg_one * arg_two + +num_215 = 42 +frac_215 = 4.2 +str_215 = "hello" + +# Polymorphic empty collections +empty_list_215 = [] + +# Mixed polymorphic structures +mixed_215 = { + numbers: { value: num_215, list: [num_215, num_215], float: frac }, + strings: { value: str_215, list: [str_215, str_215] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_215 }, + }, + computations: { + from_num: num_215 * 100, + from_frac: frac_215 * 10.0, + list_from_num: [num_215, num_215, num_215], + }, +} + +x_216 = 3.14 +y_216 = 1.23e45 +z_216 = 0.5 + +my_str_216 : Str +my_str_216 = "one" + +binops_216 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_216 : U64 -> U64 +add_one_216 = |n| n + 1 + +map_add_one_216 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_216 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_216 = |arg_one, arg_two| arg_one * arg_two + +num_216 = 42 +frac_216 = 4.2 +str_216 = "hello" + +# Polymorphic empty collections +empty_list_216 = [] + +# Mixed polymorphic structures +mixed_216 = { + numbers: { value: num_216, list: [num_216, num_216], float: frac }, + strings: { value: str_216, list: [str_216, str_216] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_216 }, + }, + computations: { + from_num: num_216 * 100, + from_frac: frac_216 * 10.0, + list_from_num: [num_216, num_216, num_216], + }, +} + +x_217 = 3.14 +y_217 = 1.23e45 +z_217 = 0.5 + +my_str_217 : Str +my_str_217 = "one" + +binops_217 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_217 : U64 -> U64 +add_one_217 = |n| n + 1 + +map_add_one_217 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_217 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_217 = |arg_one, arg_two| arg_one * arg_two + +num_217 = 42 +frac_217 = 4.2 +str_217 = "hello" + +# Polymorphic empty collections +empty_list_217 = [] + +# Mixed polymorphic structures +mixed_217 = { + numbers: { value: num_217, list: [num_217, num_217], float: frac }, + strings: { value: str_217, list: [str_217, str_217] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_217 }, + }, + computations: { + from_num: num_217 * 100, + from_frac: frac_217 * 10.0, + list_from_num: [num_217, num_217, num_217], + }, +} + +x_218 = 3.14 +y_218 = 1.23e45 +z_218 = 0.5 + +my_str_218 : Str +my_str_218 = "one" + +binops_218 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_218 : U64 -> U64 +add_one_218 = |n| n + 1 + +map_add_one_218 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_218 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_218 = |arg_one, arg_two| arg_one * arg_two + +num_218 = 42 +frac_218 = 4.2 +str_218 = "hello" + +# Polymorphic empty collections +empty_list_218 = [] + +# Mixed polymorphic structures +mixed_218 = { + numbers: { value: num_218, list: [num_218, num_218], float: frac }, + strings: { value: str_218, list: [str_218, str_218] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_218 }, + }, + computations: { + from_num: num_218 * 100, + from_frac: frac_218 * 10.0, + list_from_num: [num_218, num_218, num_218], + }, +} + +x_219 = 3.14 +y_219 = 1.23e45 +z_219 = 0.5 + +my_str_219 : Str +my_str_219 = "one" + +binops_219 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_219 : U64 -> U64 +add_one_219 = |n| n + 1 + +map_add_one_219 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_219 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_219 = |arg_one, arg_two| arg_one * arg_two + +num_219 = 42 +frac_219 = 4.2 +str_219 = "hello" + +# Polymorphic empty collections +empty_list_219 = [] + +# Mixed polymorphic structures +mixed_219 = { + numbers: { value: num_219, list: [num_219, num_219], float: frac }, + strings: { value: str_219, list: [str_219, str_219] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_219 }, + }, + computations: { + from_num: num_219 * 100, + from_frac: frac_219 * 10.0, + list_from_num: [num_219, num_219, num_219], + }, +} + +x_220 = 3.14 +y_220 = 1.23e45 +z_220 = 0.5 + +my_str_220 : Str +my_str_220 = "one" + +binops_220 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_220 : U64 -> U64 +add_one_220 = |n| n + 1 + +map_add_one_220 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_220 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_220 = |arg_one, arg_two| arg_one * arg_two + +num_220 = 42 +frac_220 = 4.2 +str_220 = "hello" + +# Polymorphic empty collections +empty_list_220 = [] + +# Mixed polymorphic structures +mixed_220 = { + numbers: { value: num_220, list: [num_220, num_220], float: frac }, + strings: { value: str_220, list: [str_220, str_220] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_220 }, + }, + computations: { + from_num: num_220 * 100, + from_frac: frac_220 * 10.0, + list_from_num: [num_220, num_220, num_220], + }, +} + +x_221 = 3.14 +y_221 = 1.23e45 +z_221 = 0.5 + +my_str_221 : Str +my_str_221 = "one" + +binops_221 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_221 : U64 -> U64 +add_one_221 = |n| n + 1 + +map_add_one_221 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_221 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_221 = |arg_one, arg_two| arg_one * arg_two + +num_221 = 42 +frac_221 = 4.2 +str_221 = "hello" + +# Polymorphic empty collections +empty_list_221 = [] + +# Mixed polymorphic structures +mixed_221 = { + numbers: { value: num_221, list: [num_221, num_221], float: frac }, + strings: { value: str_221, list: [str_221, str_221] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_221 }, + }, + computations: { + from_num: num_221 * 100, + from_frac: frac_221 * 10.0, + list_from_num: [num_221, num_221, num_221], + }, +} + +x_222 = 3.14 +y_222 = 1.23e45 +z_222 = 0.5 + +my_str_222 : Str +my_str_222 = "one" + +binops_222 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_222 : U64 -> U64 +add_one_222 = |n| n + 1 + +map_add_one_222 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_222 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_222 = |arg_one, arg_two| arg_one * arg_two + +num_222 = 42 +frac_222 = 4.2 +str_222 = "hello" + +# Polymorphic empty collections +empty_list_222 = [] + +# Mixed polymorphic structures +mixed_222 = { + numbers: { value: num_222, list: [num_222, num_222], float: frac }, + strings: { value: str_222, list: [str_222, str_222] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_222 }, + }, + computations: { + from_num: num_222 * 100, + from_frac: frac_222 * 10.0, + list_from_num: [num_222, num_222, num_222], + }, +} + +x_223 = 3.14 +y_223 = 1.23e45 +z_223 = 0.5 + +my_str_223 : Str +my_str_223 = "one" + +binops_223 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_223 : U64 -> U64 +add_one_223 = |n| n + 1 + +map_add_one_223 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_223 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_223 = |arg_one, arg_two| arg_one * arg_two + +num_223 = 42 +frac_223 = 4.2 +str_223 = "hello" + +# Polymorphic empty collections +empty_list_223 = [] + +# Mixed polymorphic structures +mixed_223 = { + numbers: { value: num_223, list: [num_223, num_223], float: frac }, + strings: { value: str_223, list: [str_223, str_223] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_223 }, + }, + computations: { + from_num: num_223 * 100, + from_frac: frac_223 * 10.0, + list_from_num: [num_223, num_223, num_223], + }, +} + +x_224 = 3.14 +y_224 = 1.23e45 +z_224 = 0.5 + +my_str_224 : Str +my_str_224 = "one" + +binops_224 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_224 : U64 -> U64 +add_one_224 = |n| n + 1 + +map_add_one_224 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_224 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_224 = |arg_one, arg_two| arg_one * arg_two + +num_224 = 42 +frac_224 = 4.2 +str_224 = "hello" + +# Polymorphic empty collections +empty_list_224 = [] + +# Mixed polymorphic structures +mixed_224 = { + numbers: { value: num_224, list: [num_224, num_224], float: frac }, + strings: { value: str_224, list: [str_224, str_224] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_224 }, + }, + computations: { + from_num: num_224 * 100, + from_frac: frac_224 * 10.0, + list_from_num: [num_224, num_224, num_224], + }, +} + +x_225 = 3.14 +y_225 = 1.23e45 +z_225 = 0.5 + +my_str_225 : Str +my_str_225 = "one" + +binops_225 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_225 : U64 -> U64 +add_one_225 = |n| n + 1 + +map_add_one_225 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_225 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_225 = |arg_one, arg_two| arg_one * arg_two + +num_225 = 42 +frac_225 = 4.2 +str_225 = "hello" + +# Polymorphic empty collections +empty_list_225 = [] + +# Mixed polymorphic structures +mixed_225 = { + numbers: { value: num_225, list: [num_225, num_225], float: frac }, + strings: { value: str_225, list: [str_225, str_225] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_225 }, + }, + computations: { + from_num: num_225 * 100, + from_frac: frac_225 * 10.0, + list_from_num: [num_225, num_225, num_225], + }, +} + +x_226 = 3.14 +y_226 = 1.23e45 +z_226 = 0.5 + +my_str_226 : Str +my_str_226 = "one" + +binops_226 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_226 : U64 -> U64 +add_one_226 = |n| n + 1 + +map_add_one_226 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_226 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_226 = |arg_one, arg_two| arg_one * arg_two + +num_226 = 42 +frac_226 = 4.2 +str_226 = "hello" + +# Polymorphic empty collections +empty_list_226 = [] + +# Mixed polymorphic structures +mixed_226 = { + numbers: { value: num_226, list: [num_226, num_226], float: frac }, + strings: { value: str_226, list: [str_226, str_226] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_226 }, + }, + computations: { + from_num: num_226 * 100, + from_frac: frac_226 * 10.0, + list_from_num: [num_226, num_226, num_226], + }, +} + +x_227 = 3.14 +y_227 = 1.23e45 +z_227 = 0.5 + +my_str_227 : Str +my_str_227 = "one" + +binops_227 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_227 : U64 -> U64 +add_one_227 = |n| n + 1 + +map_add_one_227 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_227 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_227 = |arg_one, arg_two| arg_one * arg_two + +num_227 = 42 +frac_227 = 4.2 +str_227 = "hello" + +# Polymorphic empty collections +empty_list_227 = [] + +# Mixed polymorphic structures +mixed_227 = { + numbers: { value: num_227, list: [num_227, num_227], float: frac }, + strings: { value: str_227, list: [str_227, str_227] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_227 }, + }, + computations: { + from_num: num_227 * 100, + from_frac: frac_227 * 10.0, + list_from_num: [num_227, num_227, num_227], + }, +} + +x_228 = 3.14 +y_228 = 1.23e45 +z_228 = 0.5 + +my_str_228 : Str +my_str_228 = "one" + +binops_228 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_228 : U64 -> U64 +add_one_228 = |n| n + 1 + +map_add_one_228 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_228 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_228 = |arg_one, arg_two| arg_one * arg_two + +num_228 = 42 +frac_228 = 4.2 +str_228 = "hello" + +# Polymorphic empty collections +empty_list_228 = [] + +# Mixed polymorphic structures +mixed_228 = { + numbers: { value: num_228, list: [num_228, num_228], float: frac }, + strings: { value: str_228, list: [str_228, str_228] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_228 }, + }, + computations: { + from_num: num_228 * 100, + from_frac: frac_228 * 10.0, + list_from_num: [num_228, num_228, num_228], + }, +} + +x_229 = 3.14 +y_229 = 1.23e45 +z_229 = 0.5 + +my_str_229 : Str +my_str_229 = "one" + +binops_229 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_229 : U64 -> U64 +add_one_229 = |n| n + 1 + +map_add_one_229 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_229 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_229 = |arg_one, arg_two| arg_one * arg_two + +num_229 = 42 +frac_229 = 4.2 +str_229 = "hello" + +# Polymorphic empty collections +empty_list_229 = [] + +# Mixed polymorphic structures +mixed_229 = { + numbers: { value: num_229, list: [num_229, num_229], float: frac }, + strings: { value: str_229, list: [str_229, str_229] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_229 }, + }, + computations: { + from_num: num_229 * 100, + from_frac: frac_229 * 10.0, + list_from_num: [num_229, num_229, num_229], + }, +} + +x_230 = 3.14 +y_230 = 1.23e45 +z_230 = 0.5 + +my_str_230 : Str +my_str_230 = "one" + +binops_230 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_230 : U64 -> U64 +add_one_230 = |n| n + 1 + +map_add_one_230 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_230 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_230 = |arg_one, arg_two| arg_one * arg_two + +num_230 = 42 +frac_230 = 4.2 +str_230 = "hello" + +# Polymorphic empty collections +empty_list_230 = [] + +# Mixed polymorphic structures +mixed_230 = { + numbers: { value: num_230, list: [num_230, num_230], float: frac }, + strings: { value: str_230, list: [str_230, str_230] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_230 }, + }, + computations: { + from_num: num_230 * 100, + from_frac: frac_230 * 10.0, + list_from_num: [num_230, num_230, num_230], + }, +} + +x_231 = 3.14 +y_231 = 1.23e45 +z_231 = 0.5 + +my_str_231 : Str +my_str_231 = "one" + +binops_231 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_231 : U64 -> U64 +add_one_231 = |n| n + 1 + +map_add_one_231 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_231 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_231 = |arg_one, arg_two| arg_one * arg_two + +num_231 = 42 +frac_231 = 4.2 +str_231 = "hello" + +# Polymorphic empty collections +empty_list_231 = [] + +# Mixed polymorphic structures +mixed_231 = { + numbers: { value: num_231, list: [num_231, num_231], float: frac }, + strings: { value: str_231, list: [str_231, str_231] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_231 }, + }, + computations: { + from_num: num_231 * 100, + from_frac: frac_231 * 10.0, + list_from_num: [num_231, num_231, num_231], + }, +} + +x_232 = 3.14 +y_232 = 1.23e45 +z_232 = 0.5 + +my_str_232 : Str +my_str_232 = "one" + +binops_232 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_232 : U64 -> U64 +add_one_232 = |n| n + 1 + +map_add_one_232 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_232 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_232 = |arg_one, arg_two| arg_one * arg_two + +num_232 = 42 +frac_232 = 4.2 +str_232 = "hello" + +# Polymorphic empty collections +empty_list_232 = [] + +# Mixed polymorphic structures +mixed_232 = { + numbers: { value: num_232, list: [num_232, num_232], float: frac }, + strings: { value: str_232, list: [str_232, str_232] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_232 }, + }, + computations: { + from_num: num_232 * 100, + from_frac: frac_232 * 10.0, + list_from_num: [num_232, num_232, num_232], + }, +} + +x_233 = 3.14 +y_233 = 1.23e45 +z_233 = 0.5 + +my_str_233 : Str +my_str_233 = "one" + +binops_233 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_233 : U64 -> U64 +add_one_233 = |n| n + 1 + +map_add_one_233 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_233 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_233 = |arg_one, arg_two| arg_one * arg_two + +num_233 = 42 +frac_233 = 4.2 +str_233 = "hello" + +# Polymorphic empty collections +empty_list_233 = [] + +# Mixed polymorphic structures +mixed_233 = { + numbers: { value: num_233, list: [num_233, num_233], float: frac }, + strings: { value: str_233, list: [str_233, str_233] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_233 }, + }, + computations: { + from_num: num_233 * 100, + from_frac: frac_233 * 10.0, + list_from_num: [num_233, num_233, num_233], + }, +} + +x_234 = 3.14 +y_234 = 1.23e45 +z_234 = 0.5 + +my_str_234 : Str +my_str_234 = "one" + +binops_234 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_234 : U64 -> U64 +add_one_234 = |n| n + 1 + +map_add_one_234 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_234 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_234 = |arg_one, arg_two| arg_one * arg_two + +num_234 = 42 +frac_234 = 4.2 +str_234 = "hello" + +# Polymorphic empty collections +empty_list_234 = [] + +# Mixed polymorphic structures +mixed_234 = { + numbers: { value: num_234, list: [num_234, num_234], float: frac }, + strings: { value: str_234, list: [str_234, str_234] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_234 }, + }, + computations: { + from_num: num_234 * 100, + from_frac: frac_234 * 10.0, + list_from_num: [num_234, num_234, num_234], + }, +} + +x_235 = 3.14 +y_235 = 1.23e45 +z_235 = 0.5 + +my_str_235 : Str +my_str_235 = "one" + +binops_235 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_235 : U64 -> U64 +add_one_235 = |n| n + 1 + +map_add_one_235 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_235 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_235 = |arg_one, arg_two| arg_one * arg_two + +num_235 = 42 +frac_235 = 4.2 +str_235 = "hello" + +# Polymorphic empty collections +empty_list_235 = [] + +# Mixed polymorphic structures +mixed_235 = { + numbers: { value: num_235, list: [num_235, num_235], float: frac }, + strings: { value: str_235, list: [str_235, str_235] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_235 }, + }, + computations: { + from_num: num_235 * 100, + from_frac: frac_235 * 10.0, + list_from_num: [num_235, num_235, num_235], + }, +} + +x_236 = 3.14 +y_236 = 1.23e45 +z_236 = 0.5 + +my_str_236 : Str +my_str_236 = "one" + +binops_236 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_236 : U64 -> U64 +add_one_236 = |n| n + 1 + +map_add_one_236 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_236 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_236 = |arg_one, arg_two| arg_one * arg_two + +num_236 = 42 +frac_236 = 4.2 +str_236 = "hello" + +# Polymorphic empty collections +empty_list_236 = [] + +# Mixed polymorphic structures +mixed_236 = { + numbers: { value: num_236, list: [num_236, num_236], float: frac }, + strings: { value: str_236, list: [str_236, str_236] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_236 }, + }, + computations: { + from_num: num_236 * 100, + from_frac: frac_236 * 10.0, + list_from_num: [num_236, num_236, num_236], + }, +} + +x_237 = 3.14 +y_237 = 1.23e45 +z_237 = 0.5 + +my_str_237 : Str +my_str_237 = "one" + +binops_237 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_237 : U64 -> U64 +add_one_237 = |n| n + 1 + +map_add_one_237 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_237 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_237 = |arg_one, arg_two| arg_one * arg_two + +num_237 = 42 +frac_237 = 4.2 +str_237 = "hello" + +# Polymorphic empty collections +empty_list_237 = [] + +# Mixed polymorphic structures +mixed_237 = { + numbers: { value: num_237, list: [num_237, num_237], float: frac }, + strings: { value: str_237, list: [str_237, str_237] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_237 }, + }, + computations: { + from_num: num_237 * 100, + from_frac: frac_237 * 10.0, + list_from_num: [num_237, num_237, num_237], + }, +} + +x_238 = 3.14 +y_238 = 1.23e45 +z_238 = 0.5 + +my_str_238 : Str +my_str_238 = "one" + +binops_238 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_238 : U64 -> U64 +add_one_238 = |n| n + 1 + +map_add_one_238 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_238 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_238 = |arg_one, arg_two| arg_one * arg_two + +num_238 = 42 +frac_238 = 4.2 +str_238 = "hello" + +# Polymorphic empty collections +empty_list_238 = [] + +# Mixed polymorphic structures +mixed_238 = { + numbers: { value: num_238, list: [num_238, num_238], float: frac }, + strings: { value: str_238, list: [str_238, str_238] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_238 }, + }, + computations: { + from_num: num_238 * 100, + from_frac: frac_238 * 10.0, + list_from_num: [num_238, num_238, num_238], + }, +} + +x_239 = 3.14 +y_239 = 1.23e45 +z_239 = 0.5 + +my_str_239 : Str +my_str_239 = "one" + +binops_239 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_239 : U64 -> U64 +add_one_239 = |n| n + 1 + +map_add_one_239 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_239 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_239 = |arg_one, arg_two| arg_one * arg_two + +num_239 = 42 +frac_239 = 4.2 +str_239 = "hello" + +# Polymorphic empty collections +empty_list_239 = [] + +# Mixed polymorphic structures +mixed_239 = { + numbers: { value: num_239, list: [num_239, num_239], float: frac }, + strings: { value: str_239, list: [str_239, str_239] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_239 }, + }, + computations: { + from_num: num_239 * 100, + from_frac: frac_239 * 10.0, + list_from_num: [num_239, num_239, num_239], + }, +} + +x_240 = 3.14 +y_240 = 1.23e45 +z_240 = 0.5 + +my_str_240 : Str +my_str_240 = "one" + +binops_240 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_240 : U64 -> U64 +add_one_240 = |n| n + 1 + +map_add_one_240 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_240 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_240 = |arg_one, arg_two| arg_one * arg_two + +num_240 = 42 +frac_240 = 4.2 +str_240 = "hello" + +# Polymorphic empty collections +empty_list_240 = [] + +# Mixed polymorphic structures +mixed_240 = { + numbers: { value: num_240, list: [num_240, num_240], float: frac }, + strings: { value: str_240, list: [str_240, str_240] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_240 }, + }, + computations: { + from_num: num_240 * 100, + from_frac: frac_240 * 10.0, + list_from_num: [num_240, num_240, num_240], + }, +} + +x_241 = 3.14 +y_241 = 1.23e45 +z_241 = 0.5 + +my_str_241 : Str +my_str_241 = "one" + +binops_241 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_241 : U64 -> U64 +add_one_241 = |n| n + 1 + +map_add_one_241 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_241 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_241 = |arg_one, arg_two| arg_one * arg_two + +num_241 = 42 +frac_241 = 4.2 +str_241 = "hello" + +# Polymorphic empty collections +empty_list_241 = [] + +# Mixed polymorphic structures +mixed_241 = { + numbers: { value: num_241, list: [num_241, num_241], float: frac }, + strings: { value: str_241, list: [str_241, str_241] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_241 }, + }, + computations: { + from_num: num_241 * 100, + from_frac: frac_241 * 10.0, + list_from_num: [num_241, num_241, num_241], + }, +} + +x_242 = 3.14 +y_242 = 1.23e45 +z_242 = 0.5 + +my_str_242 : Str +my_str_242 = "one" + +binops_242 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_242 : U64 -> U64 +add_one_242 = |n| n + 1 + +map_add_one_242 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_242 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_242 = |arg_one, arg_two| arg_one * arg_two + +num_242 = 42 +frac_242 = 4.2 +str_242 = "hello" + +# Polymorphic empty collections +empty_list_242 = [] + +# Mixed polymorphic structures +mixed_242 = { + numbers: { value: num_242, list: [num_242, num_242], float: frac }, + strings: { value: str_242, list: [str_242, str_242] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_242 }, + }, + computations: { + from_num: num_242 * 100, + from_frac: frac_242 * 10.0, + list_from_num: [num_242, num_242, num_242], + }, +} + +x_243 = 3.14 +y_243 = 1.23e45 +z_243 = 0.5 + +my_str_243 : Str +my_str_243 = "one" + +binops_243 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_243 : U64 -> U64 +add_one_243 = |n| n + 1 + +map_add_one_243 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_243 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_243 = |arg_one, arg_two| arg_one * arg_two + +num_243 = 42 +frac_243 = 4.2 +str_243 = "hello" + +# Polymorphic empty collections +empty_list_243 = [] + +# Mixed polymorphic structures +mixed_243 = { + numbers: { value: num_243, list: [num_243, num_243], float: frac }, + strings: { value: str_243, list: [str_243, str_243] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_243 }, + }, + computations: { + from_num: num_243 * 100, + from_frac: frac_243 * 10.0, + list_from_num: [num_243, num_243, num_243], + }, +} + +x_244 = 3.14 +y_244 = 1.23e45 +z_244 = 0.5 + +my_str_244 : Str +my_str_244 = "one" + +binops_244 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_244 : U64 -> U64 +add_one_244 = |n| n + 1 + +map_add_one_244 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_244 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_244 = |arg_one, arg_two| arg_one * arg_two + +num_244 = 42 +frac_244 = 4.2 +str_244 = "hello" + +# Polymorphic empty collections +empty_list_244 = [] + +# Mixed polymorphic structures +mixed_244 = { + numbers: { value: num_244, list: [num_244, num_244], float: frac }, + strings: { value: str_244, list: [str_244, str_244] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_244 }, + }, + computations: { + from_num: num_244 * 100, + from_frac: frac_244 * 10.0, + list_from_num: [num_244, num_244, num_244], + }, +} + +x_245 = 3.14 +y_245 = 1.23e45 +z_245 = 0.5 + +my_str_245 : Str +my_str_245 = "one" + +binops_245 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_245 : U64 -> U64 +add_one_245 = |n| n + 1 + +map_add_one_245 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_245 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_245 = |arg_one, arg_two| arg_one * arg_two + +num_245 = 42 +frac_245 = 4.2 +str_245 = "hello" + +# Polymorphic empty collections +empty_list_245 = [] + +# Mixed polymorphic structures +mixed_245 = { + numbers: { value: num_245, list: [num_245, num_245], float: frac }, + strings: { value: str_245, list: [str_245, str_245] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_245 }, + }, + computations: { + from_num: num_245 * 100, + from_frac: frac_245 * 10.0, + list_from_num: [num_245, num_245, num_245], + }, +} + +x_246 = 3.14 +y_246 = 1.23e45 +z_246 = 0.5 + +my_str_246 : Str +my_str_246 = "one" + +binops_246 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_246 : U64 -> U64 +add_one_246 = |n| n + 1 + +map_add_one_246 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_246 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_246 = |arg_one, arg_two| arg_one * arg_two + +num_246 = 42 +frac_246 = 4.2 +str_246 = "hello" + +# Polymorphic empty collections +empty_list_246 = [] + +# Mixed polymorphic structures +mixed_246 = { + numbers: { value: num_246, list: [num_246, num_246], float: frac }, + strings: { value: str_246, list: [str_246, str_246] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_246 }, + }, + computations: { + from_num: num_246 * 100, + from_frac: frac_246 * 10.0, + list_from_num: [num_246, num_246, num_246], + }, +} + +x_247 = 3.14 +y_247 = 1.23e45 +z_247 = 0.5 + +my_str_247 : Str +my_str_247 = "one" + +binops_247 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_247 : U64 -> U64 +add_one_247 = |n| n + 1 + +map_add_one_247 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_247 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_247 = |arg_one, arg_two| arg_one * arg_two + +num_247 = 42 +frac_247 = 4.2 +str_247 = "hello" + +# Polymorphic empty collections +empty_list_247 = [] + +# Mixed polymorphic structures +mixed_247 = { + numbers: { value: num_247, list: [num_247, num_247], float: frac }, + strings: { value: str_247, list: [str_247, str_247] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_247 }, + }, + computations: { + from_num: num_247 * 100, + from_frac: frac_247 * 10.0, + list_from_num: [num_247, num_247, num_247], + }, +} + +x_248 = 3.14 +y_248 = 1.23e45 +z_248 = 0.5 + +my_str_248 : Str +my_str_248 = "one" + +binops_248 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_248 : U64 -> U64 +add_one_248 = |n| n + 1 + +map_add_one_248 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_248 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_248 = |arg_one, arg_two| arg_one * arg_two + +num_248 = 42 +frac_248 = 4.2 +str_248 = "hello" + +# Polymorphic empty collections +empty_list_248 = [] + +# Mixed polymorphic structures +mixed_248 = { + numbers: { value: num_248, list: [num_248, num_248], float: frac }, + strings: { value: str_248, list: [str_248, str_248] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_248 }, + }, + computations: { + from_num: num_248 * 100, + from_frac: frac_248 * 10.0, + list_from_num: [num_248, num_248, num_248], + }, +} + +x_249 = 3.14 +y_249 = 1.23e45 +z_249 = 0.5 + +my_str_249 : Str +my_str_249 = "one" + +binops_249 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_249 : U64 -> U64 +add_one_249 = |n| n + 1 + +map_add_one_249 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_249 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_249 = |arg_one, arg_two| arg_one * arg_two + +num_249 = 42 +frac_249 = 4.2 +str_249 = "hello" + +# Polymorphic empty collections +empty_list_249 = [] + +# Mixed polymorphic structures +mixed_249 = { + numbers: { value: num_249, list: [num_249, num_249], float: frac }, + strings: { value: str_249, list: [str_249, str_249] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_249 }, + }, + computations: { + from_num: num_249 * 100, + from_frac: frac_249 * 10.0, + list_from_num: [num_249, num_249, num_249], + }, +} + +x_250 = 3.14 +y_250 = 1.23e45 +z_250 = 0.5 + +my_str_250 : Str +my_str_250 = "one" + +binops_250 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_250 : U64 -> U64 +add_one_250 = |n| n + 1 + +map_add_one_250 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_250 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_250 = |arg_one, arg_two| arg_one * arg_two + +num_250 = 42 +frac_250 = 4.2 +str_250 = "hello" + +# Polymorphic empty collections +empty_list_250 = [] + +# Mixed polymorphic structures +mixed_250 = { + numbers: { value: num_250, list: [num_250, num_250], float: frac }, + strings: { value: str_250, list: [str_250, str_250] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_250 }, + }, + computations: { + from_num: num_250 * 100, + from_frac: frac_250 * 10.0, + list_from_num: [num_250, num_250, num_250], + }, +} + +x_251 = 3.14 +y_251 = 1.23e45 +z_251 = 0.5 + +my_str_251 : Str +my_str_251 = "one" + +binops_251 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_251 : U64 -> U64 +add_one_251 = |n| n + 1 + +map_add_one_251 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_251 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_251 = |arg_one, arg_two| arg_one * arg_two + +num_251 = 42 +frac_251 = 4.2 +str_251 = "hello" + +# Polymorphic empty collections +empty_list_251 = [] + +# Mixed polymorphic structures +mixed_251 = { + numbers: { value: num_251, list: [num_251, num_251], float: frac }, + strings: { value: str_251, list: [str_251, str_251] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_251 }, + }, + computations: { + from_num: num_251 * 100, + from_frac: frac_251 * 10.0, + list_from_num: [num_251, num_251, num_251], + }, +} + +x_252 = 3.14 +y_252 = 1.23e45 +z_252 = 0.5 + +my_str_252 : Str +my_str_252 = "one" + +binops_252 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_252 : U64 -> U64 +add_one_252 = |n| n + 1 + +map_add_one_252 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_252 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_252 = |arg_one, arg_two| arg_one * arg_two + +num_252 = 42 +frac_252 = 4.2 +str_252 = "hello" + +# Polymorphic empty collections +empty_list_252 = [] + +# Mixed polymorphic structures +mixed_252 = { + numbers: { value: num_252, list: [num_252, num_252], float: frac }, + strings: { value: str_252, list: [str_252, str_252] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_252 }, + }, + computations: { + from_num: num_252 * 100, + from_frac: frac_252 * 10.0, + list_from_num: [num_252, num_252, num_252], + }, +} + +x_253 = 3.14 +y_253 = 1.23e45 +z_253 = 0.5 + +my_str_253 : Str +my_str_253 = "one" + +binops_253 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_253 : U64 -> U64 +add_one_253 = |n| n + 1 + +map_add_one_253 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_253 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_253 = |arg_one, arg_two| arg_one * arg_two + +num_253 = 42 +frac_253 = 4.2 +str_253 = "hello" + +# Polymorphic empty collections +empty_list_253 = [] + +# Mixed polymorphic structures +mixed_253 = { + numbers: { value: num_253, list: [num_253, num_253], float: frac }, + strings: { value: str_253, list: [str_253, str_253] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_253 }, + }, + computations: { + from_num: num_253 * 100, + from_frac: frac_253 * 10.0, + list_from_num: [num_253, num_253, num_253], + }, +} + +x_254 = 3.14 +y_254 = 1.23e45 +z_254 = 0.5 + +my_str_254 : Str +my_str_254 = "one" + +binops_254 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_254 : U64 -> U64 +add_one_254 = |n| n + 1 + +map_add_one_254 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_254 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_254 = |arg_one, arg_two| arg_one * arg_two + +num_254 = 42 +frac_254 = 4.2 +str_254 = "hello" + +# Polymorphic empty collections +empty_list_254 = [] + +# Mixed polymorphic structures +mixed_254 = { + numbers: { value: num_254, list: [num_254, num_254], float: frac }, + strings: { value: str_254, list: [str_254, str_254] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_254 }, + }, + computations: { + from_num: num_254 * 100, + from_frac: frac_254 * 10.0, + list_from_num: [num_254, num_254, num_254], + }, +} + +x_255 = 3.14 +y_255 = 1.23e45 +z_255 = 0.5 + +my_str_255 : Str +my_str_255 = "one" + +binops_255 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_255 : U64 -> U64 +add_one_255 = |n| n + 1 + +map_add_one_255 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_255 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_255 = |arg_one, arg_two| arg_one * arg_two + +num_255 = 42 +frac_255 = 4.2 +str_255 = "hello" + +# Polymorphic empty collections +empty_list_255 = [] + +# Mixed polymorphic structures +mixed_255 = { + numbers: { value: num_255, list: [num_255, num_255], float: frac }, + strings: { value: str_255, list: [str_255, str_255] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_255 }, + }, + computations: { + from_num: num_255 * 100, + from_frac: frac_255 * 10.0, + list_from_num: [num_255, num_255, num_255], + }, +} + +x_256 = 3.14 +y_256 = 1.23e45 +z_256 = 0.5 + +my_str_256 : Str +my_str_256 = "one" + +binops_256 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_256 : U64 -> U64 +add_one_256 = |n| n + 1 + +map_add_one_256 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_256 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_256 = |arg_one, arg_two| arg_one * arg_two + +num_256 = 42 +frac_256 = 4.2 +str_256 = "hello" + +# Polymorphic empty collections +empty_list_256 = [] + +# Mixed polymorphic structures +mixed_256 = { + numbers: { value: num_256, list: [num_256, num_256], float: frac }, + strings: { value: str_256, list: [str_256, str_256] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_256 }, + }, + computations: { + from_num: num_256 * 100, + from_frac: frac_256 * 10.0, + list_from_num: [num_256, num_256, num_256], + }, +} + +x_257 = 3.14 +y_257 = 1.23e45 +z_257 = 0.5 + +my_str_257 : Str +my_str_257 = "one" + +binops_257 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_257 : U64 -> U64 +add_one_257 = |n| n + 1 + +map_add_one_257 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_257 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_257 = |arg_one, arg_two| arg_one * arg_two + +num_257 = 42 +frac_257 = 4.2 +str_257 = "hello" + +# Polymorphic empty collections +empty_list_257 = [] + +# Mixed polymorphic structures +mixed_257 = { + numbers: { value: num_257, list: [num_257, num_257], float: frac }, + strings: { value: str_257, list: [str_257, str_257] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_257 }, + }, + computations: { + from_num: num_257 * 100, + from_frac: frac_257 * 10.0, + list_from_num: [num_257, num_257, num_257], + }, +} + +x_258 = 3.14 +y_258 = 1.23e45 +z_258 = 0.5 + +my_str_258 : Str +my_str_258 = "one" + +binops_258 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_258 : U64 -> U64 +add_one_258 = |n| n + 1 + +map_add_one_258 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_258 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_258 = |arg_one, arg_two| arg_one * arg_two + +num_258 = 42 +frac_258 = 4.2 +str_258 = "hello" + +# Polymorphic empty collections +empty_list_258 = [] + +# Mixed polymorphic structures +mixed_258 = { + numbers: { value: num_258, list: [num_258, num_258], float: frac }, + strings: { value: str_258, list: [str_258, str_258] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_258 }, + }, + computations: { + from_num: num_258 * 100, + from_frac: frac_258 * 10.0, + list_from_num: [num_258, num_258, num_258], + }, +} + +x_259 = 3.14 +y_259 = 1.23e45 +z_259 = 0.5 + +my_str_259 : Str +my_str_259 = "one" + +binops_259 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_259 : U64 -> U64 +add_one_259 = |n| n + 1 + +map_add_one_259 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_259 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_259 = |arg_one, arg_two| arg_one * arg_two + +num_259 = 42 +frac_259 = 4.2 +str_259 = "hello" + +# Polymorphic empty collections +empty_list_259 = [] + +# Mixed polymorphic structures +mixed_259 = { + numbers: { value: num_259, list: [num_259, num_259], float: frac }, + strings: { value: str_259, list: [str_259, str_259] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_259 }, + }, + computations: { + from_num: num_259 * 100, + from_frac: frac_259 * 10.0, + list_from_num: [num_259, num_259, num_259], + }, +} + +x_260 = 3.14 +y_260 = 1.23e45 +z_260 = 0.5 + +my_str_260 : Str +my_str_260 = "one" + +binops_260 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_260 : U64 -> U64 +add_one_260 = |n| n + 1 + +map_add_one_260 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_260 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_260 = |arg_one, arg_two| arg_one * arg_two + +num_260 = 42 +frac_260 = 4.2 +str_260 = "hello" + +# Polymorphic empty collections +empty_list_260 = [] + +# Mixed polymorphic structures +mixed_260 = { + numbers: { value: num_260, list: [num_260, num_260], float: frac }, + strings: { value: str_260, list: [str_260, str_260] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_260 }, + }, + computations: { + from_num: num_260 * 100, + from_frac: frac_260 * 10.0, + list_from_num: [num_260, num_260, num_260], + }, +} + +x_261 = 3.14 +y_261 = 1.23e45 +z_261 = 0.5 + +my_str_261 : Str +my_str_261 = "one" + +binops_261 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_261 : U64 -> U64 +add_one_261 = |n| n + 1 + +map_add_one_261 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_261 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_261 = |arg_one, arg_two| arg_one * arg_two + +num_261 = 42 +frac_261 = 4.2 +str_261 = "hello" + +# Polymorphic empty collections +empty_list_261 = [] + +# Mixed polymorphic structures +mixed_261 = { + numbers: { value: num_261, list: [num_261, num_261], float: frac }, + strings: { value: str_261, list: [str_261, str_261] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_261 }, + }, + computations: { + from_num: num_261 * 100, + from_frac: frac_261 * 10.0, + list_from_num: [num_261, num_261, num_261], + }, +} + +x_262 = 3.14 +y_262 = 1.23e45 +z_262 = 0.5 + +my_str_262 : Str +my_str_262 = "one" + +binops_262 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_262 : U64 -> U64 +add_one_262 = |n| n + 1 + +map_add_one_262 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_262 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_262 = |arg_one, arg_two| arg_one * arg_two + +num_262 = 42 +frac_262 = 4.2 +str_262 = "hello" + +# Polymorphic empty collections +empty_list_262 = [] + +# Mixed polymorphic structures +mixed_262 = { + numbers: { value: num_262, list: [num_262, num_262], float: frac }, + strings: { value: str_262, list: [str_262, str_262] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_262 }, + }, + computations: { + from_num: num_262 * 100, + from_frac: frac_262 * 10.0, + list_from_num: [num_262, num_262, num_262], + }, +} + +x_263 = 3.14 +y_263 = 1.23e45 +z_263 = 0.5 + +my_str_263 : Str +my_str_263 = "one" + +binops_263 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_263 : U64 -> U64 +add_one_263 = |n| n + 1 + +map_add_one_263 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_263 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_263 = |arg_one, arg_two| arg_one * arg_two + +num_263 = 42 +frac_263 = 4.2 +str_263 = "hello" + +# Polymorphic empty collections +empty_list_263 = [] + +# Mixed polymorphic structures +mixed_263 = { + numbers: { value: num_263, list: [num_263, num_263], float: frac }, + strings: { value: str_263, list: [str_263, str_263] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_263 }, + }, + computations: { + from_num: num_263 * 100, + from_frac: frac_263 * 10.0, + list_from_num: [num_263, num_263, num_263], + }, +} + +x_264 = 3.14 +y_264 = 1.23e45 +z_264 = 0.5 + +my_str_264 : Str +my_str_264 = "one" + +binops_264 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_264 : U64 -> U64 +add_one_264 = |n| n + 1 + +map_add_one_264 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_264 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_264 = |arg_one, arg_two| arg_one * arg_two + +num_264 = 42 +frac_264 = 4.2 +str_264 = "hello" + +# Polymorphic empty collections +empty_list_264 = [] + +# Mixed polymorphic structures +mixed_264 = { + numbers: { value: num_264, list: [num_264, num_264], float: frac }, + strings: { value: str_264, list: [str_264, str_264] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_264 }, + }, + computations: { + from_num: num_264 * 100, + from_frac: frac_264 * 10.0, + list_from_num: [num_264, num_264, num_264], + }, +} + +x_265 = 3.14 +y_265 = 1.23e45 +z_265 = 0.5 + +my_str_265 : Str +my_str_265 = "one" + +binops_265 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_265 : U64 -> U64 +add_one_265 = |n| n + 1 + +map_add_one_265 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_265 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_265 = |arg_one, arg_two| arg_one * arg_two + +num_265 = 42 +frac_265 = 4.2 +str_265 = "hello" + +# Polymorphic empty collections +empty_list_265 = [] + +# Mixed polymorphic structures +mixed_265 = { + numbers: { value: num_265, list: [num_265, num_265], float: frac }, + strings: { value: str_265, list: [str_265, str_265] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_265 }, + }, + computations: { + from_num: num_265 * 100, + from_frac: frac_265 * 10.0, + list_from_num: [num_265, num_265, num_265], + }, +} + +x_266 = 3.14 +y_266 = 1.23e45 +z_266 = 0.5 + +my_str_266 : Str +my_str_266 = "one" + +binops_266 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_266 : U64 -> U64 +add_one_266 = |n| n + 1 + +map_add_one_266 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_266 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_266 = |arg_one, arg_two| arg_one * arg_two + +num_266 = 42 +frac_266 = 4.2 +str_266 = "hello" + +# Polymorphic empty collections +empty_list_266 = [] + +# Mixed polymorphic structures +mixed_266 = { + numbers: { value: num_266, list: [num_266, num_266], float: frac }, + strings: { value: str_266, list: [str_266, str_266] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_266 }, + }, + computations: { + from_num: num_266 * 100, + from_frac: frac_266 * 10.0, + list_from_num: [num_266, num_266, num_266], + }, +} + +x_267 = 3.14 +y_267 = 1.23e45 +z_267 = 0.5 + +my_str_267 : Str +my_str_267 = "one" + +binops_267 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_267 : U64 -> U64 +add_one_267 = |n| n + 1 + +map_add_one_267 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_267 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_267 = |arg_one, arg_two| arg_one * arg_two + +num_267 = 42 +frac_267 = 4.2 +str_267 = "hello" + +# Polymorphic empty collections +empty_list_267 = [] + +# Mixed polymorphic structures +mixed_267 = { + numbers: { value: num_267, list: [num_267, num_267], float: frac }, + strings: { value: str_267, list: [str_267, str_267] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_267 }, + }, + computations: { + from_num: num_267 * 100, + from_frac: frac_267 * 10.0, + list_from_num: [num_267, num_267, num_267], + }, +} + +x_268 = 3.14 +y_268 = 1.23e45 +z_268 = 0.5 + +my_str_268 : Str +my_str_268 = "one" + +binops_268 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_268 : U64 -> U64 +add_one_268 = |n| n + 1 + +map_add_one_268 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_268 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_268 = |arg_one, arg_two| arg_one * arg_two + +num_268 = 42 +frac_268 = 4.2 +str_268 = "hello" + +# Polymorphic empty collections +empty_list_268 = [] + +# Mixed polymorphic structures +mixed_268 = { + numbers: { value: num_268, list: [num_268, num_268], float: frac }, + strings: { value: str_268, list: [str_268, str_268] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_268 }, + }, + computations: { + from_num: num_268 * 100, + from_frac: frac_268 * 10.0, + list_from_num: [num_268, num_268, num_268], + }, +} + +x_269 = 3.14 +y_269 = 1.23e45 +z_269 = 0.5 + +my_str_269 : Str +my_str_269 = "one" + +binops_269 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_269 : U64 -> U64 +add_one_269 = |n| n + 1 + +map_add_one_269 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_269 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_269 = |arg_one, arg_two| arg_one * arg_two + +num_269 = 42 +frac_269 = 4.2 +str_269 = "hello" + +# Polymorphic empty collections +empty_list_269 = [] + +# Mixed polymorphic structures +mixed_269 = { + numbers: { value: num_269, list: [num_269, num_269], float: frac }, + strings: { value: str_269, list: [str_269, str_269] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_269 }, + }, + computations: { + from_num: num_269 * 100, + from_frac: frac_269 * 10.0, + list_from_num: [num_269, num_269, num_269], + }, +} + +x_270 = 3.14 +y_270 = 1.23e45 +z_270 = 0.5 + +my_str_270 : Str +my_str_270 = "one" + +binops_270 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_270 : U64 -> U64 +add_one_270 = |n| n + 1 + +map_add_one_270 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_270 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_270 = |arg_one, arg_two| arg_one * arg_two + +num_270 = 42 +frac_270 = 4.2 +str_270 = "hello" + +# Polymorphic empty collections +empty_list_270 = [] + +# Mixed polymorphic structures +mixed_270 = { + numbers: { value: num_270, list: [num_270, num_270], float: frac }, + strings: { value: str_270, list: [str_270, str_270] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_270 }, + }, + computations: { + from_num: num_270 * 100, + from_frac: frac_270 * 10.0, + list_from_num: [num_270, num_270, num_270], + }, +} + +x_271 = 3.14 +y_271 = 1.23e45 +z_271 = 0.5 + +my_str_271 : Str +my_str_271 = "one" + +binops_271 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_271 : U64 -> U64 +add_one_271 = |n| n + 1 + +map_add_one_271 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_271 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_271 = |arg_one, arg_two| arg_one * arg_two + +num_271 = 42 +frac_271 = 4.2 +str_271 = "hello" + +# Polymorphic empty collections +empty_list_271 = [] + +# Mixed polymorphic structures +mixed_271 = { + numbers: { value: num_271, list: [num_271, num_271], float: frac }, + strings: { value: str_271, list: [str_271, str_271] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_271 }, + }, + computations: { + from_num: num_271 * 100, + from_frac: frac_271 * 10.0, + list_from_num: [num_271, num_271, num_271], + }, +} + +x_272 = 3.14 +y_272 = 1.23e45 +z_272 = 0.5 + +my_str_272 : Str +my_str_272 = "one" + +binops_272 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_272 : U64 -> U64 +add_one_272 = |n| n + 1 + +map_add_one_272 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_272 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_272 = |arg_one, arg_two| arg_one * arg_two + +num_272 = 42 +frac_272 = 4.2 +str_272 = "hello" + +# Polymorphic empty collections +empty_list_272 = [] + +# Mixed polymorphic structures +mixed_272 = { + numbers: { value: num_272, list: [num_272, num_272], float: frac }, + strings: { value: str_272, list: [str_272, str_272] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_272 }, + }, + computations: { + from_num: num_272 * 100, + from_frac: frac_272 * 10.0, + list_from_num: [num_272, num_272, num_272], + }, +} + +x_273 = 3.14 +y_273 = 1.23e45 +z_273 = 0.5 + +my_str_273 : Str +my_str_273 = "one" + +binops_273 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_273 : U64 -> U64 +add_one_273 = |n| n + 1 + +map_add_one_273 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_273 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_273 = |arg_one, arg_two| arg_one * arg_two + +num_273 = 42 +frac_273 = 4.2 +str_273 = "hello" + +# Polymorphic empty collections +empty_list_273 = [] + +# Mixed polymorphic structures +mixed_273 = { + numbers: { value: num_273, list: [num_273, num_273], float: frac }, + strings: { value: str_273, list: [str_273, str_273] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_273 }, + }, + computations: { + from_num: num_273 * 100, + from_frac: frac_273 * 10.0, + list_from_num: [num_273, num_273, num_273], + }, +} + +x_274 = 3.14 +y_274 = 1.23e45 +z_274 = 0.5 + +my_str_274 : Str +my_str_274 = "one" + +binops_274 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_274 : U64 -> U64 +add_one_274 = |n| n + 1 + +map_add_one_274 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_274 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_274 = |arg_one, arg_two| arg_one * arg_two + +num_274 = 42 +frac_274 = 4.2 +str_274 = "hello" + +# Polymorphic empty collections +empty_list_274 = [] + +# Mixed polymorphic structures +mixed_274 = { + numbers: { value: num_274, list: [num_274, num_274], float: frac }, + strings: { value: str_274, list: [str_274, str_274] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_274 }, + }, + computations: { + from_num: num_274 * 100, + from_frac: frac_274 * 10.0, + list_from_num: [num_274, num_274, num_274], + }, +} + +x_275 = 3.14 +y_275 = 1.23e45 +z_275 = 0.5 + +my_str_275 : Str +my_str_275 = "one" + +binops_275 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_275 : U64 -> U64 +add_one_275 = |n| n + 1 + +map_add_one_275 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_275 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_275 = |arg_one, arg_two| arg_one * arg_two + +num_275 = 42 +frac_275 = 4.2 +str_275 = "hello" + +# Polymorphic empty collections +empty_list_275 = [] + +# Mixed polymorphic structures +mixed_275 = { + numbers: { value: num_275, list: [num_275, num_275], float: frac }, + strings: { value: str_275, list: [str_275, str_275] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_275 }, + }, + computations: { + from_num: num_275 * 100, + from_frac: frac_275 * 10.0, + list_from_num: [num_275, num_275, num_275], + }, +} + +x_276 = 3.14 +y_276 = 1.23e45 +z_276 = 0.5 + +my_str_276 : Str +my_str_276 = "one" + +binops_276 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_276 : U64 -> U64 +add_one_276 = |n| n + 1 + +map_add_one_276 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_276 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_276 = |arg_one, arg_two| arg_one * arg_two + +num_276 = 42 +frac_276 = 4.2 +str_276 = "hello" + +# Polymorphic empty collections +empty_list_276 = [] + +# Mixed polymorphic structures +mixed_276 = { + numbers: { value: num_276, list: [num_276, num_276], float: frac }, + strings: { value: str_276, list: [str_276, str_276] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_276 }, + }, + computations: { + from_num: num_276 * 100, + from_frac: frac_276 * 10.0, + list_from_num: [num_276, num_276, num_276], + }, +} + +x_277 = 3.14 +y_277 = 1.23e45 +z_277 = 0.5 + +my_str_277 : Str +my_str_277 = "one" + +binops_277 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_277 : U64 -> U64 +add_one_277 = |n| n + 1 + +map_add_one_277 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_277 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_277 = |arg_one, arg_two| arg_one * arg_two + +num_277 = 42 +frac_277 = 4.2 +str_277 = "hello" + +# Polymorphic empty collections +empty_list_277 = [] + +# Mixed polymorphic structures +mixed_277 = { + numbers: { value: num_277, list: [num_277, num_277], float: frac }, + strings: { value: str_277, list: [str_277, str_277] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_277 }, + }, + computations: { + from_num: num_277 * 100, + from_frac: frac_277 * 10.0, + list_from_num: [num_277, num_277, num_277], + }, +} + +x_278 = 3.14 +y_278 = 1.23e45 +z_278 = 0.5 + +my_str_278 : Str +my_str_278 = "one" + +binops_278 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_278 : U64 -> U64 +add_one_278 = |n| n + 1 + +map_add_one_278 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_278 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_278 = |arg_one, arg_two| arg_one * arg_two + +num_278 = 42 +frac_278 = 4.2 +str_278 = "hello" + +# Polymorphic empty collections +empty_list_278 = [] + +# Mixed polymorphic structures +mixed_278 = { + numbers: { value: num_278, list: [num_278, num_278], float: frac }, + strings: { value: str_278, list: [str_278, str_278] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_278 }, + }, + computations: { + from_num: num_278 * 100, + from_frac: frac_278 * 10.0, + list_from_num: [num_278, num_278, num_278], + }, +} + +x_279 = 3.14 +y_279 = 1.23e45 +z_279 = 0.5 + +my_str_279 : Str +my_str_279 = "one" + +binops_279 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_279 : U64 -> U64 +add_one_279 = |n| n + 1 + +map_add_one_279 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_279 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_279 = |arg_one, arg_two| arg_one * arg_two + +num_279 = 42 +frac_279 = 4.2 +str_279 = "hello" + +# Polymorphic empty collections +empty_list_279 = [] + +# Mixed polymorphic structures +mixed_279 = { + numbers: { value: num_279, list: [num_279, num_279], float: frac }, + strings: { value: str_279, list: [str_279, str_279] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_279 }, + }, + computations: { + from_num: num_279 * 100, + from_frac: frac_279 * 10.0, + list_from_num: [num_279, num_279, num_279], + }, +} + +x_280 = 3.14 +y_280 = 1.23e45 +z_280 = 0.5 + +my_str_280 : Str +my_str_280 = "one" + +binops_280 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_280 : U64 -> U64 +add_one_280 = |n| n + 1 + +map_add_one_280 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_280 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_280 = |arg_one, arg_two| arg_one * arg_two + +num_280 = 42 +frac_280 = 4.2 +str_280 = "hello" + +# Polymorphic empty collections +empty_list_280 = [] + +# Mixed polymorphic structures +mixed_280 = { + numbers: { value: num_280, list: [num_280, num_280], float: frac }, + strings: { value: str_280, list: [str_280, str_280] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_280 }, + }, + computations: { + from_num: num_280 * 100, + from_frac: frac_280 * 10.0, + list_from_num: [num_280, num_280, num_280], + }, +} + +x_281 = 3.14 +y_281 = 1.23e45 +z_281 = 0.5 + +my_str_281 : Str +my_str_281 = "one" + +binops_281 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_281 : U64 -> U64 +add_one_281 = |n| n + 1 + +map_add_one_281 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_281 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_281 = |arg_one, arg_two| arg_one * arg_two + +num_281 = 42 +frac_281 = 4.2 +str_281 = "hello" + +# Polymorphic empty collections +empty_list_281 = [] + +# Mixed polymorphic structures +mixed_281 = { + numbers: { value: num_281, list: [num_281, num_281], float: frac }, + strings: { value: str_281, list: [str_281, str_281] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_281 }, + }, + computations: { + from_num: num_281 * 100, + from_frac: frac_281 * 10.0, + list_from_num: [num_281, num_281, num_281], + }, +} + +x_282 = 3.14 +y_282 = 1.23e45 +z_282 = 0.5 + +my_str_282 : Str +my_str_282 = "one" + +binops_282 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_282 : U64 -> U64 +add_one_282 = |n| n + 1 + +map_add_one_282 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_282 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_282 = |arg_one, arg_two| arg_one * arg_two + +num_282 = 42 +frac_282 = 4.2 +str_282 = "hello" + +# Polymorphic empty collections +empty_list_282 = [] + +# Mixed polymorphic structures +mixed_282 = { + numbers: { value: num_282, list: [num_282, num_282], float: frac }, + strings: { value: str_282, list: [str_282, str_282] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_282 }, + }, + computations: { + from_num: num_282 * 100, + from_frac: frac_282 * 10.0, + list_from_num: [num_282, num_282, num_282], + }, +} + +x_283 = 3.14 +y_283 = 1.23e45 +z_283 = 0.5 + +my_str_283 : Str +my_str_283 = "one" + +binops_283 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_283 : U64 -> U64 +add_one_283 = |n| n + 1 + +map_add_one_283 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_283 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_283 = |arg_one, arg_two| arg_one * arg_two + +num_283 = 42 +frac_283 = 4.2 +str_283 = "hello" + +# Polymorphic empty collections +empty_list_283 = [] + +# Mixed polymorphic structures +mixed_283 = { + numbers: { value: num_283, list: [num_283, num_283], float: frac }, + strings: { value: str_283, list: [str_283, str_283] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_283 }, + }, + computations: { + from_num: num_283 * 100, + from_frac: frac_283 * 10.0, + list_from_num: [num_283, num_283, num_283], + }, +} + +x_284 = 3.14 +y_284 = 1.23e45 +z_284 = 0.5 + +my_str_284 : Str +my_str_284 = "one" + +binops_284 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_284 : U64 -> U64 +add_one_284 = |n| n + 1 + +map_add_one_284 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_284 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_284 = |arg_one, arg_two| arg_one * arg_two + +num_284 = 42 +frac_284 = 4.2 +str_284 = "hello" + +# Polymorphic empty collections +empty_list_284 = [] + +# Mixed polymorphic structures +mixed_284 = { + numbers: { value: num_284, list: [num_284, num_284], float: frac }, + strings: { value: str_284, list: [str_284, str_284] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_284 }, + }, + computations: { + from_num: num_284 * 100, + from_frac: frac_284 * 10.0, + list_from_num: [num_284, num_284, num_284], + }, +} + +x_285 = 3.14 +y_285 = 1.23e45 +z_285 = 0.5 + +my_str_285 : Str +my_str_285 = "one" + +binops_285 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_285 : U64 -> U64 +add_one_285 = |n| n + 1 + +map_add_one_285 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_285 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_285 = |arg_one, arg_two| arg_one * arg_two + +num_285 = 42 +frac_285 = 4.2 +str_285 = "hello" + +# Polymorphic empty collections +empty_list_285 = [] + +# Mixed polymorphic structures +mixed_285 = { + numbers: { value: num_285, list: [num_285, num_285], float: frac }, + strings: { value: str_285, list: [str_285, str_285] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_285 }, + }, + computations: { + from_num: num_285 * 100, + from_frac: frac_285 * 10.0, + list_from_num: [num_285, num_285, num_285], + }, +} + +x_286 = 3.14 +y_286 = 1.23e45 +z_286 = 0.5 + +my_str_286 : Str +my_str_286 = "one" + +binops_286 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_286 : U64 -> U64 +add_one_286 = |n| n + 1 + +map_add_one_286 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_286 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_286 = |arg_one, arg_two| arg_one * arg_two + +num_286 = 42 +frac_286 = 4.2 +str_286 = "hello" + +# Polymorphic empty collections +empty_list_286 = [] + +# Mixed polymorphic structures +mixed_286 = { + numbers: { value: num_286, list: [num_286, num_286], float: frac }, + strings: { value: str_286, list: [str_286, str_286] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_286 }, + }, + computations: { + from_num: num_286 * 100, + from_frac: frac_286 * 10.0, + list_from_num: [num_286, num_286, num_286], + }, +} + +x_287 = 3.14 +y_287 = 1.23e45 +z_287 = 0.5 + +my_str_287 : Str +my_str_287 = "one" + +binops_287 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_287 : U64 -> U64 +add_one_287 = |n| n + 1 + +map_add_one_287 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_287 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_287 = |arg_one, arg_two| arg_one * arg_two + +num_287 = 42 +frac_287 = 4.2 +str_287 = "hello" + +# Polymorphic empty collections +empty_list_287 = [] + +# Mixed polymorphic structures +mixed_287 = { + numbers: { value: num_287, list: [num_287, num_287], float: frac }, + strings: { value: str_287, list: [str_287, str_287] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_287 }, + }, + computations: { + from_num: num_287 * 100, + from_frac: frac_287 * 10.0, + list_from_num: [num_287, num_287, num_287], + }, +} + +x_288 = 3.14 +y_288 = 1.23e45 +z_288 = 0.5 + +my_str_288 : Str +my_str_288 = "one" + +binops_288 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_288 : U64 -> U64 +add_one_288 = |n| n + 1 + +map_add_one_288 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_288 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_288 = |arg_one, arg_two| arg_one * arg_two + +num_288 = 42 +frac_288 = 4.2 +str_288 = "hello" + +# Polymorphic empty collections +empty_list_288 = [] + +# Mixed polymorphic structures +mixed_288 = { + numbers: { value: num_288, list: [num_288, num_288], float: frac }, + strings: { value: str_288, list: [str_288, str_288] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_288 }, + }, + computations: { + from_num: num_288 * 100, + from_frac: frac_288 * 10.0, + list_from_num: [num_288, num_288, num_288], + }, +} + +x_289 = 3.14 +y_289 = 1.23e45 +z_289 = 0.5 + +my_str_289 : Str +my_str_289 = "one" + +binops_289 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_289 : U64 -> U64 +add_one_289 = |n| n + 1 + +map_add_one_289 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_289 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_289 = |arg_one, arg_two| arg_one * arg_two + +num_289 = 42 +frac_289 = 4.2 +str_289 = "hello" + +# Polymorphic empty collections +empty_list_289 = [] + +# Mixed polymorphic structures +mixed_289 = { + numbers: { value: num_289, list: [num_289, num_289], float: frac }, + strings: { value: str_289, list: [str_289, str_289] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_289 }, + }, + computations: { + from_num: num_289 * 100, + from_frac: frac_289 * 10.0, + list_from_num: [num_289, num_289, num_289], + }, +} + +x_290 = 3.14 +y_290 = 1.23e45 +z_290 = 0.5 + +my_str_290 : Str +my_str_290 = "one" + +binops_290 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_290 : U64 -> U64 +add_one_290 = |n| n + 1 + +map_add_one_290 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_290 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_290 = |arg_one, arg_two| arg_one * arg_two + +num_290 = 42 +frac_290 = 4.2 +str_290 = "hello" + +# Polymorphic empty collections +empty_list_290 = [] + +# Mixed polymorphic structures +mixed_290 = { + numbers: { value: num_290, list: [num_290, num_290], float: frac }, + strings: { value: str_290, list: [str_290, str_290] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_290 }, + }, + computations: { + from_num: num_290 * 100, + from_frac: frac_290 * 10.0, + list_from_num: [num_290, num_290, num_290], + }, +} + +x_291 = 3.14 +y_291 = 1.23e45 +z_291 = 0.5 + +my_str_291 : Str +my_str_291 = "one" + +binops_291 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_291 : U64 -> U64 +add_one_291 = |n| n + 1 + +map_add_one_291 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_291 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_291 = |arg_one, arg_two| arg_one * arg_two + +num_291 = 42 +frac_291 = 4.2 +str_291 = "hello" + +# Polymorphic empty collections +empty_list_291 = [] + +# Mixed polymorphic structures +mixed_291 = { + numbers: { value: num_291, list: [num_291, num_291], float: frac }, + strings: { value: str_291, list: [str_291, str_291] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_291 }, + }, + computations: { + from_num: num_291 * 100, + from_frac: frac_291 * 10.0, + list_from_num: [num_291, num_291, num_291], + }, +} + +x_292 = 3.14 +y_292 = 1.23e45 +z_292 = 0.5 + +my_str_292 : Str +my_str_292 = "one" + +binops_292 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_292 : U64 -> U64 +add_one_292 = |n| n + 1 + +map_add_one_292 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_292 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_292 = |arg_one, arg_two| arg_one * arg_two + +num_292 = 42 +frac_292 = 4.2 +str_292 = "hello" + +# Polymorphic empty collections +empty_list_292 = [] + +# Mixed polymorphic structures +mixed_292 = { + numbers: { value: num_292, list: [num_292, num_292], float: frac }, + strings: { value: str_292, list: [str_292, str_292] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_292 }, + }, + computations: { + from_num: num_292 * 100, + from_frac: frac_292 * 10.0, + list_from_num: [num_292, num_292, num_292], + }, +} + +x_293 = 3.14 +y_293 = 1.23e45 +z_293 = 0.5 + +my_str_293 : Str +my_str_293 = "one" + +binops_293 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_293 : U64 -> U64 +add_one_293 = |n| n + 1 + +map_add_one_293 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_293 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_293 = |arg_one, arg_two| arg_one * arg_two + +num_293 = 42 +frac_293 = 4.2 +str_293 = "hello" + +# Polymorphic empty collections +empty_list_293 = [] + +# Mixed polymorphic structures +mixed_293 = { + numbers: { value: num_293, list: [num_293, num_293], float: frac }, + strings: { value: str_293, list: [str_293, str_293] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_293 }, + }, + computations: { + from_num: num_293 * 100, + from_frac: frac_293 * 10.0, + list_from_num: [num_293, num_293, num_293], + }, +} + +x_294 = 3.14 +y_294 = 1.23e45 +z_294 = 0.5 + +my_str_294 : Str +my_str_294 = "one" + +binops_294 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_294 : U64 -> U64 +add_one_294 = |n| n + 1 + +map_add_one_294 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_294 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_294 = |arg_one, arg_two| arg_one * arg_two + +num_294 = 42 +frac_294 = 4.2 +str_294 = "hello" + +# Polymorphic empty collections +empty_list_294 = [] + +# Mixed polymorphic structures +mixed_294 = { + numbers: { value: num_294, list: [num_294, num_294], float: frac }, + strings: { value: str_294, list: [str_294, str_294] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_294 }, + }, + computations: { + from_num: num_294 * 100, + from_frac: frac_294 * 10.0, + list_from_num: [num_294, num_294, num_294], + }, +} + +x_295 = 3.14 +y_295 = 1.23e45 +z_295 = 0.5 + +my_str_295 : Str +my_str_295 = "one" + +binops_295 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_295 : U64 -> U64 +add_one_295 = |n| n + 1 + +map_add_one_295 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_295 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_295 = |arg_one, arg_two| arg_one * arg_two + +num_295 = 42 +frac_295 = 4.2 +str_295 = "hello" + +# Polymorphic empty collections +empty_list_295 = [] + +# Mixed polymorphic structures +mixed_295 = { + numbers: { value: num_295, list: [num_295, num_295], float: frac }, + strings: { value: str_295, list: [str_295, str_295] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_295 }, + }, + computations: { + from_num: num_295 * 100, + from_frac: frac_295 * 10.0, + list_from_num: [num_295, num_295, num_295], + }, +} + +x_296 = 3.14 +y_296 = 1.23e45 +z_296 = 0.5 + +my_str_296 : Str +my_str_296 = "one" + +binops_296 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_296 : U64 -> U64 +add_one_296 = |n| n + 1 + +map_add_one_296 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_296 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_296 = |arg_one, arg_two| arg_one * arg_two + +num_296 = 42 +frac_296 = 4.2 +str_296 = "hello" + +# Polymorphic empty collections +empty_list_296 = [] + +# Mixed polymorphic structures +mixed_296 = { + numbers: { value: num_296, list: [num_296, num_296], float: frac }, + strings: { value: str_296, list: [str_296, str_296] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_296 }, + }, + computations: { + from_num: num_296 * 100, + from_frac: frac_296 * 10.0, + list_from_num: [num_296, num_296, num_296], + }, +} + +x_297 = 3.14 +y_297 = 1.23e45 +z_297 = 0.5 + +my_str_297 : Str +my_str_297 = "one" + +binops_297 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_297 : U64 -> U64 +add_one_297 = |n| n + 1 + +map_add_one_297 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_297 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_297 = |arg_one, arg_two| arg_one * arg_two + +num_297 = 42 +frac_297 = 4.2 +str_297 = "hello" + +# Polymorphic empty collections +empty_list_297 = [] + +# Mixed polymorphic structures +mixed_297 = { + numbers: { value: num_297, list: [num_297, num_297], float: frac }, + strings: { value: str_297, list: [str_297, str_297] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_297 }, + }, + computations: { + from_num: num_297 * 100, + from_frac: frac_297 * 10.0, + list_from_num: [num_297, num_297, num_297], + }, +} + +x_298 = 3.14 +y_298 = 1.23e45 +z_298 = 0.5 + +my_str_298 : Str +my_str_298 = "one" + +binops_298 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_298 : U64 -> U64 +add_one_298 = |n| n + 1 + +map_add_one_298 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_298 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_298 = |arg_one, arg_two| arg_one * arg_two + +num_298 = 42 +frac_298 = 4.2 +str_298 = "hello" + +# Polymorphic empty collections +empty_list_298 = [] + +# Mixed polymorphic structures +mixed_298 = { + numbers: { value: num_298, list: [num_298, num_298], float: frac }, + strings: { value: str_298, list: [str_298, str_298] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_298 }, + }, + computations: { + from_num: num_298 * 100, + from_frac: frac_298 * 10.0, + list_from_num: [num_298, num_298, num_298], + }, +} + +x_299 = 3.14 +y_299 = 1.23e45 +z_299 = 0.5 + +my_str_299 : Str +my_str_299 = "one" + +binops_299 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_299 : U64 -> U64 +add_one_299 = |n| n + 1 + +map_add_one_299 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_299 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_299 = |arg_one, arg_two| arg_one * arg_two + +num_299 = 42 +frac_299 = 4.2 +str_299 = "hello" + +# Polymorphic empty collections +empty_list_299 = [] + +# Mixed polymorphic structures +mixed_299 = { + numbers: { value: num_299, list: [num_299, num_299], float: frac }, + strings: { value: str_299, list: [str_299, str_299] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_299 }, + }, + computations: { + from_num: num_299 * 100, + from_frac: frac_299 * 10.0, + list_from_num: [num_299, num_299, num_299], + }, +} + +x_300 = 3.14 +y_300 = 1.23e45 +z_300 = 0.5 + +my_str_300 : Str +my_str_300 = "one" + +binops_300 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_300 : U64 -> U64 +add_one_300 = |n| n + 1 + +map_add_one_300 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_300 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_300 = |arg_one, arg_two| arg_one * arg_two + +num_300 = 42 +frac_300 = 4.2 +str_300 = "hello" + +# Polymorphic empty collections +empty_list_300 = [] + +# Mixed polymorphic structures +mixed_300 = { + numbers: { value: num_300, list: [num_300, num_300], float: frac }, + strings: { value: str_300, list: [str_300, str_300] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_300 }, + }, + computations: { + from_num: num_300 * 100, + from_frac: frac_300 * 10.0, + list_from_num: [num_300, num_300, num_300], + }, +} + +x_301 = 3.14 +y_301 = 1.23e45 +z_301 = 0.5 + +my_str_301 : Str +my_str_301 = "one" + +binops_301 = ( + 4 + 2, + 4 - 2, + 4 * 2, + 4 / 2, + 4 % 2, + 4 < 2, + 4 > 2, + 4 <= 2, + 4 >= 2, + 4 == 2, + 4 != 2, + 4 // 2, +) + +add_one_301 : U64 -> U64 +add_one_301 = |n| n + 1 + +map_add_one_301 = |list| { + fn = |numy| numy + 1 + list.map(fn) +} + +# Function showing var vs regular identifier independence +test_func_301 = |input| { + sum = input # Regular identifier + var sum_ = input * 2 # Var with underscore - should not conflict + + sum_ = sum_ + sum # Reassign var - should work + sum + sum_ # Both should be accessible +} + +multiply_301 = |arg_one, arg_two| arg_one * arg_two + +num_301 = 42 +frac_301 = 4.2 +str_301 = "hello" + +# Polymorphic empty collections +empty_list_301 = [] + +# Mixed polymorphic structures +mixed_301 = { + numbers: { value: num_301, list: [num_301, num_301], float: frac }, + strings: { value: str_301, list: [str_301, str_301] }, + empty_lists: { + raw: empty_list, + in_list: [empty_list], + in_record: { data: empty_list_301 }, + }, + computations: { + from_num: num_301 * 100, + from_frac: frac_301 * 10.0, + list_from_num: [num_301, num_301, num_301], + }, +} diff --git a/src/PROFILING/exec_bench.roc b/src/PROFILING/exec_bench.roc index c89361762e..6a9d35599a 100644 --- a/src/PROFILING/exec_bench.roc +++ b/src/PROFILING/exec_bench.roc @@ -1,4 +1,4 @@ -app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz7V9YurCSTFcFdu6cQJie4guzsPMUi5kBYUk.tar.br" } +app [main!] { cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br" } import cli.Arg exposing [Arg] import cli.File @@ -32,23 +32,39 @@ main! = |raw_args| median_results = calculate_medians(all_timing_data) # calculate bench file hash so we're aware of changes - bench_file_hash_out = run_cmd_w_output!("sha256sum", ["src/PROFILING/bench_repeated_check.roc"])? + bench_file_hash_out = + Cmd.new("sha256sum") + |> Cmd.arg("src/PROFILING/bench_repeated_check.roc") + |> Cmd.exec_output!()? + bench_file_hash = - bench_file_hash_out + bench_file_hash_out.stdout_utf8 |> Str.split_on(" ") |> List.get(0)? # Get the current commit hash - commit_hash_out = run_cmd_w_output!("git", ["rev-parse", "HEAD"])? - commit_hash = Str.trim(commit_hash_out) + commit_hash_out = + Cmd.new("git") + |> Cmd.args(["rev-parse", "HEAD"]) + |> Cmd.exec_output!()? + + commit_hash = Str.trim(commit_hash_out.stdout_utf8) # Get zig version - zig_version_out = run_cmd_w_output!("zig", ["version"])? - zig_version = Str.trim(zig_version_out) + zig_version_out = + Cmd.new("zig") + |> Cmd.arg("version") + |> Cmd.exec_output!()? + + zig_version = Str.trim(zig_version_out.stdout_utf8) # Get operating system with version - operating_system_out = run_cmd_w_output!("uname", ["-sr"])? - operating_system = Str.trim(operating_system_out) + operating_system_out = + Cmd.new("uname") + |> Cmd.args(["-sr"]) + |> Cmd.exec_output!()? + + operating_system = Str.trim(operating_system_out.stdout_utf8) # Create the AllBenchmarkData record benchmark_data : AllBenchmarkData @@ -66,7 +82,12 @@ main! = |raw_args| run_benchmark_command! : {} => Result Str _ run_benchmark_command! = |{}| - run_cmd_w_output!("./zig-out/bin/roc", ["check", "src/PROFILING/bench_repeated_check.roc", "--time", "--no-cache"]) + bench_output = + Cmd.new("./zig-out/bin/roc") + |> Cmd.args(["check", "src/PROFILING/bench_repeated_check.roc", "--time", "--no-cache"]) + |> Cmd.exec_output!()? + + Ok(bench_output.stdout_utf8) parse_bench_stdout : Str -> Result TimingData _ parse_bench_stdout = |output| @@ -258,30 +279,6 @@ AllBenchmarkData : { median_results : MedianResults, } -run_cmd_w_output! : Str, List Str => Result Str [BadCmdOutput(Str)]_ -run_cmd_w_output! = |cmd_str, args| - cmd_out = - Cmd.new(cmd_str) - |> Cmd.args(args) - |> Cmd.output!() - - stdout_utf8 = Str.from_utf8_lossy(cmd_out.stdout) - - when cmd_out.status is - Ok(0) -> - Ok(stdout_utf8) - _ -> - stderr_utf8 = Str.from_utf8_lossy(cmd_out.stderr) - err_data = - """ - Cmd `${cmd_str} ${Str.join_with(args, " ")}` failed: - - status: ${Inspect.to_str(cmd_out.status)} - - stdout: ${stdout_utf8} - - stderr: ${stderr_utf8} - """ - - Err(BadCmdOutput(err_data)) - # Test functions expect test_lines = [ diff --git a/src/README.md b/src/README.md index b022137383..0d07a46280 100644 --- a/src/README.md +++ b/src/README.md @@ -52,7 +52,7 @@ Try it with `zig build -Dno-bin -fincremental --watch` ### Expanding to ZLS This fast config can also be used with `zls`. Simply follow these steps: -1. run `zls --version` and make sure it is `0.14.0` (this is still used for zig `0.14.1`). +1. run `zls --version` and make sure it is `0.15.0` (this is still used for zig `0.15.2`). 2. run `zls env` and grab the `config_file` path. 3. Edit the config file to include ```json diff --git a/src/base/CommonEnv.zig b/src/base/CommonEnv.zig index 681fe79c6a..8c0f63b165 100644 --- a/src/base/CommonEnv.zig +++ b/src/base/CommonEnv.zig @@ -21,7 +21,6 @@ const CompactWriter = collections.CompactWriter; const CommonEnv = @This(); idents: Ident.Store, -// ident_ids_for_slicing: SafeList(Ident.Idx), strings: StringLiteral.Store, /// The items (a combination of types and values) that this module exposes exposed_items: ExposedItems, @@ -47,6 +46,7 @@ pub fn deinit(self: *CommonEnv, gpa: std.mem.Allocator) void { self.strings.deinit(gpa); self.exposed_items.deinit(gpa); self.line_starts.deinit(gpa); + // NOTE: Caller owns source and is responsible for freeing it. } /// Add the given offset to the memory addresses of all pointers in `self`. @@ -56,7 +56,15 @@ pub fn relocate(self: *CommonEnv, offset: isize) void { self.strings.relocate(offset); self.exposed_items.relocate(offset); self.line_starts.relocate(offset); - // Note: source is not relocated - it should be set manually + // Relocate source slice pointer if it is non-empty. + // The underlying bytes live in the same allocation as the rest of the + // module data (e.g. shared memory used by the interpreter), so we can + // adjust the pointer by the same offset. + if (self.source.len > 0) { + const old_ptr = @intFromPtr(self.source.ptr); + const new_ptr = @as(isize, @intCast(old_ptr)) + offset; + self.source.ptr = @ptrFromInt(@as(usize, @intCast(new_ptr))); + } } /// Serialize this CommonEnv to the given CompactWriter. @@ -83,20 +91,14 @@ pub fn serialize( return @constCast(offset_self); } -/// Freezes the identifier and string interners, preventing further modifications. -/// This is used to ensure thread safety when sharing the environment across threads. -pub fn freezeInterners(self: *CommonEnv) void { - self.idents.freeze(); - self.strings.freeze(); -} - -/// Serialized representation of ModuleEnv -pub const Serialized = struct { +/// Serialized representation of CommonEnv +/// Uses extern struct to guarantee consistent field layout across optimization levels. +pub const Serialized = extern struct { idents: Ident.Store.Serialized, strings: StringLiteral.Store.Serialized, exposed_items: ExposedItems.Serialized, line_starts: SafeList(u32).Serialized, - source: []const u8, // Serialized as zeros, provided during deserialization + source: [2]u64, // Reserve space for slice (ptr + len), provided during deserialization /// Serialize a ModuleEnv into this Serialized struct, appending data to the writer pub fn serialize( @@ -105,13 +107,15 @@ pub const Serialized = struct { allocator: std.mem.Allocator, writer: *CompactWriter, ) !void { - self.source = ""; // Empty slice - // Serialize each component using its Serialized struct try self.idents.serialize(&env.idents, allocator, writer); try self.strings.serialize(&env.strings, allocator, writer); try self.exposed_items.serialize(&env.exposed_items, allocator, writer); try self.line_starts.serialize(&env.line_starts, allocator, writer); + + // Set source to all zeros; the space needs to be here, + // but the value will be set separately during deserialization. + self.source = .{ 0, 0 }; } /// Deserialize a CommonEnv from the buffer, updating the CommonEnv in place @@ -120,18 +124,23 @@ pub const Serialized = struct { offset: i64, source: []const u8, ) *CommonEnv { - // CommonEnv.Serialized should be at least as big as CommonEnv - std.debug.assert(@sizeOf(Serialized) >= @sizeOf(CommonEnv)); - - // Overwrite ourself with the deserialized version, and return our pointer after casting it to CommonEnv. + // Note: Serialized may be smaller than the runtime struct because: + // - Uses i64 offsets instead of usize pointers (same size on 64-bit, but conceptually different) + // - May have different alignment/padding requirements + // We deserialize by overwriting the Serialized memory with the runtime struct. const env = @as(*CommonEnv, @ptrFromInt(@intFromPtr(self))); + // Read values BEFORE any writes to avoid corruption from in-place deserialization + const idents_val = self.idents.deserialize(offset).*; + const strings_val = self.strings.deserialize(offset).*; + const exposed_items_val = self.exposed_items.deserialize(offset).*; + const line_starts_val = self.line_starts.deserialize(offset).*; + env.* = CommonEnv{ - .idents = self.idents.deserialize(offset).*, - // .ident_ids_for_slicing = self.ident_ids_for_slicing.deserialize(offset).*, - .strings = self.strings.deserialize(offset).*, - .exposed_items = self.exposed_items.deserialize(offset).*, - .line_starts = self.line_starts.deserialize(offset).*, + .idents = idents_val, + .strings = strings_val, + .exposed_items = exposed_items_val, + .line_starts = line_starts_val, .source = source, }; @@ -389,7 +398,7 @@ test "CommonEnv.Serialized roundtrip with large data" { const gpa = testing.allocator; // Create a larger source with many lines - var source_builder = std.ArrayList(u8).init(gpa); + var source_builder = std.array_list.Managed(u8).init(gpa); defer source_builder.deinit(); for (0..100) |i| { @@ -402,11 +411,11 @@ test "CommonEnv.Serialized roundtrip with large data" { defer original.deinit(gpa); // Add many identifiers - var ident_indices = std.ArrayList(Ident.Idx).init(gpa); + var ident_indices = std.array_list.Managed(Ident.Idx).init(gpa); defer ident_indices.deinit(); for (0..50) |i| { - var ident_name = std.ArrayList(u8).init(gpa); + var ident_name = std.array_list.Managed(u8).init(gpa); defer ident_name.deinit(); try ident_name.writer().print("ident_{}", .{i}); const idx = try original.insertIdent(gpa, Ident.for_text(ident_name.items)); @@ -414,11 +423,11 @@ test "CommonEnv.Serialized roundtrip with large data" { } // Add many strings and track their indices - var string_indices = std.ArrayList(StringLiteral.Idx).init(gpa); + var string_indices = std.array_list.Managed(StringLiteral.Idx).init(gpa); defer string_indices.deinit(); for (0..25) |i| { - var string_content = std.ArrayList(u8).init(gpa); + var string_content = std.array_list.Managed(u8).init(gpa); defer string_content.deinit(); try string_content.writer().print("string_literal_{}", .{i}); const idx = try original.insertString(gpa, string_content.items); diff --git a/src/base/DataSpan.zig b/src/base/DataSpan.zig index ec41f0b0aa..295939138f 100644 --- a/src/base/DataSpan.zig +++ b/src/base/DataSpan.zig @@ -1,22 +1,23 @@ //! Just a small struct to take a span of data in an array -const DataSpan = @This(); +/// DataSpan is used for serialization, so it must be extern struct for consistent layout. +pub const DataSpan = extern struct { + start: u32, + len: u32, -start: u32, -len: u32, + /// Creates an empty DataSpan with zero start and zero length. + pub fn empty() DataSpan { + return DataSpan{ .start = 0, .len = 0 }; + } -/// Creates an empty DataSpan with zero start and zero length. -pub fn empty() DataSpan { - return DataSpan{ .start = 0, .len = 0 }; -} + /// Creates a DataSpan with the specified start position and length. + pub fn init(start: u32, len: u32) DataSpan { + return DataSpan{ .start = start, .len = len }; + } -/// Creates a DataSpan with the specified start position and length. -pub fn init(start: u32, len: u32) DataSpan { - return DataSpan{ .start = start, .len = len }; -} - -/// Converts this DataSpan into a type that contains a span field. -/// This is useful for creating wrapper types around DataSpan. -pub fn as(self: DataSpan, comptime T: type) T { - return @as(T, .{ .span = self }); -} + /// Converts this DataSpan into a type that contains a span field. + /// This is useful for creating wrapper types around DataSpan. + pub fn as(self: DataSpan, comptime T: type) T { + return @as(T, .{ .span = self }); + } +}; diff --git a/src/base/Ident.zig b/src/base/Ident.zig index b299916477..e4d9671c9b 100644 --- a/src/base/Ident.zig +++ b/src/base/Ident.zig @@ -6,6 +6,7 @@ //! in constant time. Storing IDs in each IR instead of strings also uses less memory in the IRs. const std = @import("std"); +const builtin = @import("builtin"); const serialization = @import("serialization"); const collections = @import("collections"); @@ -16,6 +17,21 @@ const CompactWriter = collections.CompactWriter; const Ident = @This(); +/// Whether to enable debug store tracking. This adds runtime checks to verify +/// that Idx values are only looked up in the store that created them. +/// Disabled on freestanding targets where threading primitives aren't available. +const is_freestanding = builtin.os.tag == .freestanding; +const enable_store_tracking = builtin.mode == .Debug and !is_freestanding; + +/// Method name for parsing integers from digit lists - used by numeric literal type checking +pub const FROM_INT_DIGITS_METHOD_NAME = "from_int_digits"; +/// Method name for parsing decimals from digit lists - used by numeric literal type checking +pub const FROM_DEC_DIGITS_METHOD_NAME = "from_dec_digits"; +/// Method name for addition - used by + operator desugaring +pub const PLUS_METHOD_NAME = "plus"; +/// Method name for negation - used by unary - operator desugaring +pub const NEGATE_METHOD_NAME = "negate"; + /// The original text of the identifier. raw_text: []const u8, @@ -72,6 +88,13 @@ pub fn from_bytes(bytes: []const u8) Error!Ident { pub const Idx = packed struct(u32) { attributes: Attributes, idx: u29, + + /// Sentinel value representing no/unset ident + pub const NONE: Idx = .{ .attributes = .{ .effectful = true, .ignored = true, .reassignable = true }, .idx = std.math.maxInt(u29) }; + + pub fn isNone(self: Idx) bool { + return self.idx == NONE.idx and @as(u3, @bitCast(self.attributes)) == @as(u3, @bitCast(NONE.attributes)); + } }; /// Identifier attributes such as if it is effectful, ignored, or reassignable. @@ -84,19 +107,167 @@ pub const Attributes = packed struct(u3) { return .{ .effectful = std.mem.endsWith(u8, text, "!"), .ignored = std.mem.startsWith(u8, text, "_"), - .reassignable = false, + .reassignable = std.mem.startsWith(u8, text, "$"), }; } }; +/// Debug-only info for store provenance tracking. +const StoreDebugInfo = struct { + store_id: []const u8, + known_idxs: std.AutoHashMapUnmanaged(u32, void), +}; + +/// Global counter for generating unique store IDs. +/// This counter survives struct copies because the ID is stored in the Store struct itself. +/// Using u32 for cross-platform compatibility (wasm32 doesn't support 64-bit atomics). +var debug_store_id_counter: if (enable_store_tracking) std.atomic.Value(u32) else void = + if (enable_store_tracking) std.atomic.Value(u32).init(1) else {}; + +/// Global map from Store's unique debug_id to debug info. +/// Protected by a mutex for thread safety. +var debug_store_map: if (enable_store_tracking) std.AutoHashMapUnmanaged(u32, StoreDebugInfo) else void = if (enable_store_tracking) .{} else {}; + +/// Mutex protecting the debug_store_map. +var debug_store_mutex: if (enable_store_tracking) std.Thread.Mutex else void = if (enable_store_tracking) .{} else {}; + /// An interner for identifier names. pub const Store = struct { interner: SmallStringInterner, attributes: collections.SafeList(Attributes) = .{}, next_unique_name: u32 = 0, + /// Debug-only: unique ID for this store instance. + /// This ID is assigned on first insert and survives struct copies. + /// 0 means unassigned. + debug_id: if (enable_store_tracking) u32 else void = if (enable_store_tracking) 0 else {}, + + /// Debug-only: get or assign a unique ID for this store. + fn getOrAssignDebugId(self: *Store, src: std.builtin.SourceLocation) u32 { + if (enable_store_tracking) { + if (self.debug_id == 0) { + // If this store already has idents (e.g., deserialized), we can't + // fully track it because existing idents weren't registered. + // Keep debug_id at 0 to skip verification for this store. + if (self.interner.entry_count > 0) { + return 0; + } + + // Assign a new unique ID + self.debug_id = debug_store_id_counter.fetchAdd(1, .monotonic); + + // Register in the global map with source location info + const store_id = std.fmt.allocPrint(std.heap.page_allocator, "{s}:{d}:{d}", .{ + src.file, + src.line, + src.column, + }) catch "unknown"; + + debug_store_map.put(std.heap.page_allocator, self.debug_id, .{ + .store_id = store_id, + .known_idxs = .{}, + }) catch {}; + } + return self.debug_id; + } else { + return 0; + } + } + + /// Debug-only: unregister this store from the global debug map. + fn unregisterFromTracking(self: *Store) void { + if (enable_store_tracking) { + if (self.debug_id == 0) return; // Never registered + + debug_store_mutex.lock(); + defer debug_store_mutex.unlock(); + + if (debug_store_map.fetchRemove(self.debug_id)) |entry| { + // Free the heap-allocated store_id (if it's not the static "unknown" string) + if (entry.value.store_id.ptr != @as([*]const u8, "unknown".ptr)) { + std.heap.page_allocator.free(entry.value.store_id); + } + // Copy the known_idxs to make it mutable for deinit + var known_idxs = entry.value.known_idxs; + known_idxs.deinit(std.heap.page_allocator); + } + } + } + + /// Debug-only: track an Idx as belonging to this store. + fn trackIdx(self: *Store, idx: Idx, src: std.builtin.SourceLocation) void { + if (enable_store_tracking) { + debug_store_mutex.lock(); + defer debug_store_mutex.unlock(); + + const debug_id = self.getOrAssignDebugId(src); + if (debug_store_map.getPtr(debug_id)) |info| { + // We don't fail on OOM in debug tracking - just skip tracking + info.known_idxs.put(std.heap.page_allocator, @bitCast(idx), {}) catch {}; + } + } + } + + /// Debug-only: verify an Idx belongs to this store. + fn verifyIdx(self: *const Store, idx: Idx) void { + if (enable_store_tracking) { + if (self.debug_id == 0) { + // Store was never registered (e.g., deserialized store). + // Skip verification. + return; + } + + debug_store_mutex.lock(); + defer debug_store_mutex.unlock(); + + const info = debug_store_map.get(self.debug_id) orelse { + // Store not in map (shouldn't happen if debug_id != 0) + return; + }; + + const idx_bits: u32 = @bitCast(idx); + if (!info.known_idxs.contains(idx_bits)) { + std.debug.panic( + "Ident.Idx lookup in wrong store: Idx {d} (0x{x}) not found in store '{s}' (debug_id={d}). " ++ + "This Idx was created by a different store.", + .{ idx.idx, idx_bits, info.store_id, self.debug_id }, + ); + } + } + } + + /// Check if an Idx was created by this store. + /// In debug builds with store tracking enabled, this checks the known_idxs set. + /// In release builds or when tracking is disabled, this returns true (assumes valid). + /// Use this to determine which store to use for lookups when idents may come from + /// multiple sources (e.g., during type unification with builtins). + pub fn containsIdx(self: *const Store, idx: Idx) bool { + if (enable_store_tracking) { + if (self.debug_id == 0) { + // Store was never registered (e.g., deserialized store). + // Can't verify, assume true. + return true; + } + + debug_store_mutex.lock(); + defer debug_store_mutex.unlock(); + + const info = debug_store_map.get(self.debug_id) orelse { + // Store not in map + return true; + }; + + const idx_bits: u32 = @bitCast(idx); + return info.known_idxs.contains(idx_bits); + } else { + // No tracking, can't determine - assume true + return true; + } + } + /// Serialized representation of an Ident.Store - pub const Serialized = struct { + /// Uses extern struct to guarantee consistent field layout across optimization levels. + pub const Serialized = extern struct { interner: SmallStringInterner.Serialized, attributes: collections.SafeList(Attributes).Serialized, next_unique_name: u32, @@ -115,23 +286,37 @@ pub const Store = struct { /// Deserialize this Serialized struct into a Store pub fn deserialize(self: *Serialized, offset: i64) *Store { - // Ident.Store.Serialized should be at least as big as Ident.Store - std.debug.assert(@sizeOf(Serialized) >= @sizeOf(Store)); - - // Overwrite ourself with the deserialized version, and return our pointer after casting it to Self. + // Note: Serialized may be smaller than the runtime struct. + // We deserialize by overwriting the Serialized memory with the runtime struct. const store = @as(*Store, @ptrFromInt(@intFromPtr(self))); + // Check struct sizes - if Store > Serialized, we'd write past the end! + comptime { + const store_size = @sizeOf(Store); + const serialized_size = @sizeOf(Serialized); + if (store_size > serialized_size) { + @compileError(std.fmt.comptimePrint( + "STRUCT SIZE MISMATCH: Store ({d} bytes) > Serialized ({d} bytes). " ++ + "Writing Store to Serialized memory will corrupt adjacent data!", + .{ store_size, serialized_size }, + )); + } + } + store.* = Store{ .interner = self.interner.deserialize(offset).*, .attributes = self.attributes.deserialize(offset).*, .next_unique_name = self.next_unique_name, }; + // Note: We don't register deserialized stores for debug tracking. + // This is fine because the debug tracking is meant to catch bugs during fresh compilation. + return store; } }; - /// Initialize the memory for an `Ident.Store` with a specific capaicty. + /// Initialize the memory for an `Ident.Store` with a specific capacity. pub fn initCapacity(gpa: std.mem.Allocator, capacity: usize) std.mem.Allocator.Error!Store { return .{ .interner = try SmallStringInterner.initCapacity(gpa, capacity), @@ -142,12 +327,30 @@ pub const Store = struct { pub fn deinit(self: *Store, gpa: std.mem.Allocator) void { self.interner.deinit(gpa); self.attributes.deinit(gpa); + self.unregisterFromTracking(); } /// Insert a new identifier into the store. pub fn insert(self: *Store, gpa: std.mem.Allocator, ident: Ident) std.mem.Allocator.Error!Idx { const idx = try self.interner.insert(gpa, ident.raw_text); + const result = Idx{ + .attributes = ident.attributes, + .idx = @as(u29, @intCast(@intFromEnum(idx))), + }; + + self.trackIdx(result, @src()); + + return result; + } + + /// Look up an identifier in the store without inserting. + /// Returns the index if found, null if not found. + /// Unlike insert, this never modifies the store (no resize, no insertion). + /// Useful for deserialized stores that cannot be grown. + pub fn lookup(self: *const Store, ident: Ident) ?Idx { + const idx = self.interner.lookup(ident.raw_text) orelse return null; + return Idx{ .attributes = ident.attributes, .idx = @as(u29, @intCast(@intFromEnum(idx))), @@ -196,14 +399,19 @@ pub const Store = struct { _ = try self.attributes.append(gpa, attributes); - return Idx{ + const result = Idx{ .attributes = attributes, .idx = @truncate(@intFromEnum(idx)), }; + + self.trackIdx(result, @src()); + + return result; } /// Get the text for an identifier. pub fn getText(self: *const Store, idx: Idx) []u8 { + self.verifyIdx(idx); return self.interner.getText(@enumFromInt(@as(u32, idx.idx))); } @@ -226,10 +434,6 @@ pub const Store = struct { }; } - /// Freeze the identifier store, preventing any new entries from being added. - pub fn freeze(self: *Store) void { - self.interner.freeze(); - } /// Calculate the size needed to serialize this Ident.Store pub fn serializedSize(self: *const Store) usize { var size: usize = 0; @@ -242,7 +446,7 @@ pub const Store = struct { size += @sizeOf(u32); // next_unique_name // Align to SERIALIZATION_ALIGNMENT to maintain alignment for subsequent data - return std.mem.alignForward(usize, size, collections.SERIALIZATION_ALIGNMENT); + return std.mem.alignForward(usize, size, collections.SERIALIZATION_ALIGNMENT.toByteUnits()); } /// Serialize this Store to the given CompactWriter. The resulting Store @@ -325,6 +529,14 @@ test "from_bytes creates ignored identifier" { try std.testing.expect(result.attributes.reassignable == false); } +test "from_bytes creates reassignable identifier" { + const result = try Ident.from_bytes("$reusable"); + try std.testing.expectEqualStrings("$reusable", result.raw_text); + try std.testing.expect(result.attributes.effectful == false); + try std.testing.expect(result.attributes.ignored == false); + try std.testing.expect(result.attributes.reassignable == true); +} + test "Ident.Store empty CompactWriter roundtrip" { const gpa = std.testing.allocator; @@ -359,7 +571,7 @@ test "Ident.Store empty CompactWriter roundtrip" { // Ensure file size matches what we wrote try std.testing.expectEqual(@as(u64, @intCast(writer.total_bytes)), file_size); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); const bytes_read = try file.read(buffer); @@ -428,7 +640,7 @@ test "Ident.Store basic CompactWriter roundtrip" { // Ensure file size matches what we wrote try std.testing.expectEqual(@as(u64, @intCast(writer.total_bytes)), file_size); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); const bytes_read = try file.read(buffer); @@ -512,7 +724,7 @@ test "Ident.Store with genUnique CompactWriter roundtrip" { // Ensure file size matches what we wrote try std.testing.expectEqual(@as(u64, @intCast(writer.total_bytes)), file_size); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); const bytes_read = try file.read(buffer); @@ -535,7 +747,7 @@ test "Ident.Store with genUnique CompactWriter roundtrip" { try std.testing.expectEqual(@as(u32, 3), deserialized.next_unique_name); } -test "Ident.Store frozen state CompactWriter roundtrip" { +test "Ident.Store CompactWriter roundtrip" { const gpa = std.testing.allocator; // Create and populate store @@ -545,14 +757,6 @@ test "Ident.Store frozen state CompactWriter roundtrip" { _ = try original.insert(gpa, Ident.for_text("test1")); _ = try original.insert(gpa, Ident.for_text("test2")); - // Freeze the store - original.freeze(); - - // Verify interner is frozen - if (std.debug.runtime_safety) { - try std.testing.expect(original.interner.frozen); - } - // Create a temp file var tmp_dir = std.testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -580,7 +784,7 @@ test "Ident.Store frozen state CompactWriter roundtrip" { // Ensure file size matches what we wrote try std.testing.expectEqual(@as(u64, @intCast(writer.total_bytes)), file_size); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); const bytes_read = try file.read(buffer); @@ -591,11 +795,6 @@ test "Ident.Store frozen state CompactWriter roundtrip" { const deserialized = @as(*Ident.Store, @ptrCast(@alignCast(buffer.ptr))); deserialized.relocate(@as(isize, @intCast(@intFromPtr(buffer.ptr)))); - - // Verify frozen state is preserved - if (std.debug.runtime_safety) { - try std.testing.expect(deserialized.interner.frozen); - } } test "Ident.Store comprehensive CompactWriter roundtrip" { @@ -621,13 +820,13 @@ test "Ident.Store comprehensive CompactWriter roundtrip" { .{ .text = "hello", .expected_idx = 1 }, // duplicate, should reuse }; - var indices = std.ArrayList(Ident.Idx).init(gpa); - defer indices.deinit(); + var indices = std.ArrayList(Ident.Idx).empty; + defer indices.deinit(gpa); for (test_idents) |test_ident| { const ident = Ident.for_text(test_ident.text); const idx = try original.insert(gpa, ident); - try indices.append(idx); + try indices.append(gpa, idx); // Verify the index matches expectation try std.testing.expectEqual(test_ident.expected_idx, idx.idx); } @@ -666,7 +865,7 @@ test "Ident.Store comprehensive CompactWriter roundtrip" { // Ensure file size matches what we wrote try std.testing.expectEqual(@as(u64, @intCast(writer.total_bytes)), file_size); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); const bytes_read = try file.read(buffer); diff --git a/src/base/PackedDataSpan.zig b/src/base/PackedDataSpan.zig index 2412ec5313..7882883435 100644 --- a/src/base/PackedDataSpan.zig +++ b/src/base/PackedDataSpan.zig @@ -11,7 +11,7 @@ const std = @import("std"); const testing = std.testing; -const DataSpan = @import("DataSpan.zig"); +const DataSpan = @import("DataSpan.zig").DataSpan; /// Configurable packed DataSpan with customizable bit allocation pub fn PackedDataSpan(comptime start_bits: u6, comptime length_bits: u6) type { diff --git a/src/base/Region.zig b/src/base/Region.zig index 326f6c4ba9..dabd915b7a 100644 --- a/src/base/Region.zig +++ b/src/base/Region.zig @@ -46,11 +46,7 @@ pub fn isEmpty(self: Self) bool { } /// Write the debug format of a region to a writer. -pub fn format(self: *const Self, comptime fmt: []const u8, _: std.fmt.FormatOptions, writer: std.io.AnyWriter) !void { - if (fmt.len != 0) { - std.fmt.invalidFmtError(fmt, self); - } - +pub fn format(self: *const Self, writer: *std.Io.Writer) std.Io.Writer.Error!void { try writer.print("@{}-{}", .{ self.start.offset, self.end.offset }); } diff --git a/src/base/SExprTree.zig b/src/base/SExprTree.zig index 7d1c5e3fda..33b212ed5a 100644 --- a/src/base/SExprTree.zig +++ b/src/base/SExprTree.zig @@ -20,6 +20,12 @@ pub const Color = enum { punctuation, }; +/// Controls whether line/column information is included in output +pub const LineColMode = enum { + skip_linecol, + include_linecol, +}; + /// Helper function to escape HTML characters fn escapeHtmlChar(writer: anytype, char: u8) !void { switch (char) { @@ -33,57 +39,53 @@ fn escapeHtmlChar(writer: anytype, char: u8) !void { } /// Plain text writer implementation -const PlainTextSExprWriter = struct { - writer: std.io.AnyWriter, +fn PlainTextSExprWriter(comptime WriterType: type) type { + return struct { + writer: WriterType, - pub fn print(self: *@This(), comptime fmt: []const u8, args: anytype) !void { - try self.writer.print(fmt, args); - } - - pub fn setColor(self: *@This(), color: Color) !void { - _ = self; - _ = color; - // No-op for plain text - } - - pub fn beginSourceRange(self: *@This(), start_byte: u32, end_byte: u32) !void { - _ = self; - _ = start_byte; - _ = end_byte; - // No-op for plain text - } - - pub fn endSourceRange(self: *@This()) !void { - _ = self; - // No-op for plain text - } - - pub fn writeIndent(self: *@This(), tabs: usize) !void { - for (0..tabs) |_| { - try self.writer.writeAll("\t"); + pub fn print(self: *@This(), comptime fmt: []const u8, args: anytype) !void { + try self.writer.print(fmt, args); } - } -}; + + pub fn setColor(_: *@This(), _: Color) !void { + // No-op for plain text + } + + pub fn beginSourceRange(_: *@This(), _: u32, _: u32) !void { + // No-op for plain text + } + + pub fn endSourceRange(_: *@This()) !void { + // No-op for plain text + } + + pub fn writeIndent(self: *@This(), tabs: usize) !void { + for (0..tabs) |_| { + try self.writer.writeAll("\t"); + } + } + }; +} /// HTML writer implementation with syntax highlighting const HtmlSExprWriter = struct { - writer: std.io.AnyWriter, + writer: *std.Io.Writer, current_color: Color = .default, color_active: bool = false, - scratch_buffer: std.ArrayList(u8), + scratch_buffer: std.array_list.Managed(u8), - pub fn init(writer: std.io.AnyWriter) HtmlSExprWriter { + pub fn init(writer: *std.Io.Writer) HtmlSExprWriter { return HtmlSExprWriter{ .writer = writer, .current_color = .default, .color_active = false, - .scratch_buffer = std.ArrayList(u8).init(std.heap.page_allocator), + .scratch_buffer = std.array_list.Managed(u8).init(std.heap.page_allocator), }; } pub fn print(self: *@This(), comptime fmt: []const u8, args: anytype) !void { self.scratch_buffer.clearRetainingCapacity(); - try std.fmt.format(self.scratch_buffer.writer(), fmt, args); + try self.scratch_buffer.print(fmt, args); for (self.scratch_buffer.items) |char| { try escapeHtmlChar(self.writer, char); @@ -144,37 +146,37 @@ const Node = union(enum) { BytesRange: struct { begin: u32, end: u32, region: RegionInfo }, }; -children: std.ArrayListUnmanaged(Node), -data: std.ArrayListUnmanaged(u8), -stack: std.ArrayListUnmanaged(Node), +children: std.array_list.Managed(Node), +data: std.array_list.Managed(u8), +stack: std.array_list.Managed(Node), allocator: std.mem.Allocator, pub fn init(allocator: std.mem.Allocator) SExprTree { return SExprTree{ - .children = .{}, - .data = .{}, - .stack = .{}, + .children = std.array_list.Managed(Node).init(allocator), + .data = std.array_list.Managed(u8).init(allocator), + .stack = std.array_list.Managed(Node).init(allocator), .allocator = allocator, }; } pub fn deinit(self: *SExprTree) void { - self.children.deinit(self.allocator); - self.data.deinit(self.allocator); - self.stack.deinit(self.allocator); + self.children.deinit(); + self.data.deinit(); + self.stack.deinit(); } /// Push a static atom (e.g. node name) onto the stack pub fn pushStaticAtom(self: *SExprTree, value: []const u8) std.mem.Allocator.Error!void { - try self.stack.append(self.allocator, Node{ .StaticAtom = value }); + try self.stack.append(Node{ .StaticAtom = value }); } /// Push a string (copied into data buffer) onto the stack pub fn pushString(self: *SExprTree, value: []const u8) std.mem.Allocator.Error!void { const begin: u32 = @intCast(self.data.items.len); - try self.data.appendSlice(self.allocator, value); + try self.data.appendSlice(value); const end: u32 = @intCast(self.data.items.len); - try self.stack.append(self.allocator, Node{ .String = .{ .begin = begin, .end = end } }); + try self.stack.append(Node{ .String = .{ .begin = begin, .end = end } }); } /// Push a string key-value pair onto the stack @@ -189,9 +191,9 @@ pub fn pushStringPair(self: *SExprTree, key: []const u8, value: []const u8) std. /// Push a dynamic atom (copied into data buffer) onto the stack pub fn pushDynamicAtom(self: *SExprTree, value: []const u8) std.mem.Allocator.Error!void { const begin: u32 = @intCast(self.data.items.len); - try self.data.appendSlice(self.allocator, value); + try self.data.appendSlice(value); const end: u32 = @intCast(self.data.items.len); - try self.stack.append(self.allocator, Node{ .DynamicAtom = .{ .begin = begin, .end = end } }); + try self.stack.append(Node{ .DynamicAtom = .{ .begin = begin, .end = end } }); } /// Push a dynamic atom key-value pair onto the stack @@ -205,7 +207,7 @@ pub fn pushDynamicAtomPair(self: *SExprTree, key: []const u8, value: []const u8) /// Push a boolean node onto the stack pub fn pushBool(self: *SExprTree, value: bool) std.mem.Allocator.Error!void { - try self.stack.append(self.allocator, Node{ .Boolean = value }); + try self.stack.append(Node{ .Boolean = value }); } /// Push a boolean key-value pair onto the stack @@ -219,12 +221,12 @@ pub fn pushBoolPair(self: *SExprTree, key: []const u8, value: bool) std.mem.Allo /// Push a NodeIdx node onto the stack pub fn pushNodeIdx(self: *SExprTree, idx: u32) std.mem.Allocator.Error!void { - try self.stack.append(self.allocator, Node{ .NodeIdx = idx }); + try self.stack.append(Node{ .NodeIdx = idx }); } /// Push a BytesRange node onto the stack pub fn pushBytesRange(self: *SExprTree, begin: u32, end: u32, region: RegionInfo) std.mem.Allocator.Error!void { - try self.stack.append(self.allocator, Node{ .BytesRange = .{ .begin = begin, .end = end, .region = region } }); + try self.stack.append(Node{ .BytesRange = .{ .begin = begin, .end = end, .region = region } }); } /// Begin a new node, returning a marker for the current stack position @@ -244,18 +246,18 @@ pub fn endNode(self: *SExprTree, begin: NodeBegin, attrsMarker: NodeBegin) std.m const children_begin: u32 = @intCast(self.children.items.len); for (self.stack.items[start_idx..total]) |node| { - try self.children.append(self.allocator, node); + try self.children.append(node); } const children_end: u32 = @intCast(self.children.items.len); const attrs_end = children_begin + (attrs_end_idx - start_idx); // Remove items from stack self.stack.shrinkRetainingCapacity(start_idx); - try self.stack.append(self.allocator, Node{ .List = .{ .begin = children_begin, .attrs_marker = attrs_end, .end = children_end } }); + try self.stack.append(Node{ .List = .{ .begin = children_begin, .attrs_marker = attrs_end, .end = children_end } }); } /// Internal method that writes the node using a writer implementation -fn toStringImpl(self: *const SExprTree, node: Node, writer_impl: anytype, indent: usize) !void { +fn toStringImpl(self: *const SExprTree, node: Node, writer_impl: anytype, indent: usize, linecol_mode: LineColMode) !void { switch (node) { .StaticAtom => |s| { try writer_impl.setColor(.node_name); @@ -290,7 +292,6 @@ fn toStringImpl(self: *const SExprTree, node: Node, writer_impl: anytype, indent }, .BytesRange => |range| { try writer_impl.beginSourceRange(range.begin, range.end); - // try writer_impl.print("@{d}-{d}", .{ range.begin, range.end }); try writer_impl.print("@{d}.{d}-{d}.{d}", .{ // add one to display numbers instead of index range.region.start_line_idx + 1, @@ -307,20 +308,36 @@ fn toStringImpl(self: *const SExprTree, node: Node, writer_impl: anytype, indent var first = true; for (range.begin..range.attrs_marker) |i| { + const child = self.children.items[i]; + + // Skip BytesRange nodes when linecol_mode is .skip_linecol + // Note we do this check here to prevent trailing whitespace in the output + if (child == .BytesRange and linecol_mode == .skip_linecol) { + continue; + } + if (!first) { try writer_impl.print(" ", .{}); } first = false; - try self.toStringImpl(self.children.items[i], writer_impl, indent + 1); + try self.toStringImpl(child, writer_impl, indent + 1, linecol_mode); } for (range.attrs_marker..range.end) |i| { + const child = self.children.items[i]; + + // Skip BytesRange nodes when linecol_mode is .skip_linecol + // Note we do this check here to prevent extra newlines in the output + if (child == .BytesRange and linecol_mode == .skip_linecol) { + continue; + } + if (!first) { try writer_impl.print("\n", .{}); try writer_impl.writeIndent(indent + 1); } first = false; - try self.toStringImpl(self.children.items[i], writer_impl, indent + 1); + try self.toStringImpl(child, writer_impl, indent + 1, linecol_mode); } try writer_impl.setColor(.punctuation); @@ -331,24 +348,24 @@ fn toStringImpl(self: *const SExprTree, node: Node, writer_impl: anytype, indent } /// Pretty-print the root node (top of stack) to the writer -pub fn printTree(self: *const SExprTree, writer: anytype) !void { +pub fn printTree(self: *const SExprTree, writer: anytype, linecol_mode: LineColMode) !void { if (self.stack.items.len == 0) return; - var plain_writer = PlainTextSExprWriter{ .writer = writer.any() }; - try self.toStringImpl(self.stack.items[self.stack.items.len - 1], &plain_writer, 0); + var plain_writer = PlainTextSExprWriter(@TypeOf(writer.any())){ .writer = writer.any() }; + try self.toStringImpl(self.stack.items[self.stack.items.len - 1], &plain_writer, 0, linecol_mode); } /// Render this SExprTree to a writer with pleasing indentation. -pub fn toStringPretty(self: *const SExprTree, writer: std.io.AnyWriter) !void { +pub fn toStringPretty(self: *const SExprTree, writer: anytype, linecol_mode: LineColMode) !void { if (self.stack.items.len == 0) return; - var plain_writer = PlainTextSExprWriter{ .writer = writer }; - try self.toStringImpl(self.stack.items[self.stack.items.len - 1], &plain_writer, 0); + var plain_writer = PlainTextSExprWriter(@TypeOf(writer)){ .writer = writer }; + try self.toStringImpl(self.stack.items[self.stack.items.len - 1], &plain_writer, 0, linecol_mode); } /// Render this SExprTree to HTML with syntax highlighting. -pub fn toHtml(self: *const SExprTree, writer: std.io.AnyWriter) !void { +pub fn toHtml(self: *const SExprTree, writer: *std.Io.Writer, linecol_mode: LineColMode) !void { if (self.stack.items.len == 0) return; var html_writer = HtmlSExprWriter.init(writer); - try self.toStringImpl(self.stack.items[self.stack.items.len - 1], &html_writer, 0); + try self.toStringImpl(self.stack.items[self.stack.items.len - 1], &html_writer, 0, linecol_mode); html_writer.deinit() catch { return error.ErrFinalizingHTMLWriter; }; diff --git a/src/base/Scratch.zig b/src/base/Scratch.zig index 7ba7004cb6..be66e67477 100644 --- a/src/base/Scratch.zig +++ b/src/base/Scratch.zig @@ -2,15 +2,15 @@ //! when working with recursive operations const std = @import("std"); -const DataSpan = @import("DataSpan.zig"); +const DataSpan = @import("DataSpan.zig").DataSpan; /// A stack for easily adding and removing index types when doing recursive operations pub fn Scratch(comptime T: type) type { return struct { - items: std.ArrayListUnmanaged(T), + items: std.array_list.Managed(T), const Self = @This(); - const ArrayList = std.ArrayListUnmanaged(T); + const ArrayList = std.array_list.Managed(T); pub fn init(gpa: std.mem.Allocator) std.mem.Allocator.Error!Self { const items = try ArrayList.initCapacity(gpa, std.math.ceilPowerOfTwoAssert(usize, 64)); @@ -19,18 +19,96 @@ pub fn Scratch(comptime T: type) type { }; } - pub fn deinit(self: *Self, gpa: std.mem.Allocator) void { - self.items.deinit(gpa); + pub fn deinit(self: *Self) void { + self.items.deinit(); } /// Returns the start position for a new Span of indexes in scratch - pub fn top(self: *Self) u32 { + pub fn top(self: *const Self) u32 { return @as(u32, @intCast(self.items.items.len)); } - /// Places a new index of type `T` in the scratch. Will panic on OOM. - pub fn append(self: *Self, gpa: std.mem.Allocator, idx: T) std.mem.Allocator.Error!void { - try self.items.append(gpa, idx); + /// Check if a value is in the array + pub fn contains(self: *const Self, val: T) bool { + for (self.items.items) |item| { + if (item == val) { + return true; + } + } + return false; + } + + /// Check if a value is in the array starting from a given position. + /// Note: If checking multiple values against the same range, use `setViewFrom()` + /// to build a SetView once and call `contains()` on it multiple times. + pub fn containsFrom(self: *const Self, start: u32, val: T) bool { + const range = self.items.items[@intCast(start)..]; + for (range) |item| { + if (item == val) { + return true; + } + } + return false; + } + + /// A view into a range of the scratch buffer optimized for membership queries. + /// For small ranges, uses linear scan. For larger ranges, uses a hash set. + pub const SetView = struct { + range: []const T, + set: ?std.AutoHashMapUnmanaged(T, void), + + const hash_threshold = 16; + + pub fn init(items: []const T) SetView { + if (items.len <= hash_threshold) { + return .{ .range = items, .set = null }; + } + var set = std.AutoHashMapUnmanaged(T, void){}; + set.ensureTotalCapacity(std.heap.page_allocator, @intCast(items.len)) catch { + // Fall back to linear scan on allocation failure + return .{ .range = items, .set = null }; + }; + for (items) |item| { + set.putAssumeCapacity(item, {}); + } + return .{ .range = items, .set = set }; + } + + pub fn deinit(self: *SetView) void { + if (self.set) |*set| { + set.deinit(std.heap.page_allocator); + } + } + + pub fn contains(self: *const SetView, val: T) bool { + if (self.set) |set| { + return set.contains(val); + } + for (self.range) |item| { + if (item == val) { + return true; + } + } + return false; + } + }; + + /// Create a SetView for efficient repeated membership queries on a range. + /// For small ranges, the SetView uses linear scan. + /// For larger ranges, it builds a hash set for O(1) lookups. + /// Remember to call deinit() on the returned SetView when done. + pub fn setViewFrom(self: *const Self, start: u32) SetView { + return SetView.init(self.items.items[@intCast(start)..]); + } + + /// Places a new index of type `T` in the scratch + pub fn append(self: *Self, idx: T) std.mem.Allocator.Error!void { + try self.items.append(idx); + } + + /// Pop an item of the scratch buffer + pub fn pop(self: *Self) ?T { + return self.items.pop(); } /// Creates slice from the provided indexes @@ -43,15 +121,34 @@ pub fn Scratch(comptime T: type) type { return self.items.items[@intCast(start)..]; } + /// Creates slice from the provided start index + pub fn sliceFromSpan(self: *Self, span: DataSpan) []T { + const start: usize = @intCast(span.start); + const end: usize = @intCast(span.start + span.len); + + std.debug.assert(start <= end); + std.debug.assert(end <= self.items.items.len); + + return self.items.items[start..end]; + } + + /// Creates span from the provided start index to the end of the list + pub fn spanFrom(self: *Self, start: u32) DataSpan { + return DataSpan{ + .start = start, + .len = @as(u32, @intCast(self.items.items.len)) - start, + }; + } + /// Creates a new span starting at start. Moves the items from scratch /// to extra_data as appropriate. - pub fn spanFromStart(self: *Self, start: u32, gpa: std.mem.Allocator, data: *std.ArrayListUnmanaged(u32)) std.mem.Allocator.Error!DataSpan { + pub fn spanFromStart(self: *Self, start: u32, data: *std.array_list.Managed(u32)) std.mem.Allocator.Error!DataSpan { const end = self.items.len; defer self.items.shrinkRetainingCapacity(start); var i = @as(usize, @intCast(start)); const data_start = @as(u32, @intCast(data.items.len)); while (i < end) { - data.append(gpa, self.items[i].id); + try data.append(self.items[i].id); i += 1; } return .{ .span = .{ .start = data_start, .len = @as(u32, @intCast(end)) - start } }; @@ -61,7 +158,9 @@ pub fn Scratch(comptime T: type) type { /// Should be used wherever the scratch items will not be used, /// as in when parsing fails. pub fn clearFrom(self: *Self, start: u32) void { - self.items.shrinkRetainingCapacity(start); + if (self.items.items.len > start) { + self.items.shrinkRetainingCapacity(start); + } } }; } diff --git a/src/base/SmallStringInterner.zig b/src/base/SmallStringInterner.zig index bfa7f44752..8f4a8af2ce 100644 --- a/src/base/SmallStringInterner.zig +++ b/src/base/SmallStringInterner.zig @@ -25,9 +25,9 @@ bytes: collections.SafeList(u8) = .{}, hash_table: collections.SafeList(Idx) = .{}, /// The current number of entries in the hash table. entry_count: u32 = 0, -/// When true, no new entries can be added to the interner. -/// This is set after parsing is complete. -frozen: if (std.debug.runtime_safety) bool else void = if (std.debug.runtime_safety) false else {}, +/// Debug-only flag to catch invalid inserts into deserialized interners. +/// Deserialized interners should never have inserts - if they do, it's a bug. +supports_inserts: if (std.debug.runtime_safety) bool else void = if (std.debug.runtime_safety) true else {}, /// A unique index for a deduped string in this interner. pub const Idx = enum(u32) { @@ -66,9 +66,44 @@ pub fn initCapacity(gpa: std.mem.Allocator, capacity: usize) std.mem.Allocator.E return self; } +/// Enable inserts on a deserialized interner for runtime use. +/// Normally deserialized interners are read-only, but the interpreter needs to +/// insert new identifiers at runtime for type name translation and method lookup. +/// This copies the deserialized data into newly allocated memory that can be grown. +/// Call this after deserialization but before using the interner for runtime operations. +pub fn enableRuntimeInserts(self: *SmallStringInterner, gpa: std.mem.Allocator) std.mem.Allocator.Error!void { + // Copy the bytes array into newly allocated memory + const bytes_slice = self.bytes.items.items; + var new_bytes = try collections.SafeList(u8).initCapacity(gpa, bytes_slice.len); + if (bytes_slice.len > 0) { + @memcpy(new_bytes.items.items.ptr, bytes_slice); + new_bytes.items.items.len = bytes_slice.len; + } + self.bytes = new_bytes; + + // Copy the hash_table array into newly allocated memory + const hash_table_slice = self.hash_table.items.items; + var new_hash_table = try collections.SafeList(Idx).initCapacity(gpa, hash_table_slice.len); + if (hash_table_slice.len > 0) { + @memcpy(new_hash_table.items.items.ptr, hash_table_slice); + new_hash_table.items.items.len = hash_table_slice.len; + } + self.hash_table = new_hash_table; + + if (std.debug.runtime_safety) { + self.supports_inserts = true; + } +} + /// Free all memory consumed by this interner. /// Will invalidate all slices referencing the interner. +/// NOTE: Do NOT call deinit on deserialized interners - their memory is owned by the deserialization buffer. pub fn deinit(self: *SmallStringInterner, gpa: std.mem.Allocator) void { + if (std.debug.runtime_safety) { + if (!self.supports_inserts) { + @panic("deinit called on deserialized interner - memory is owned by deserialization buffer"); + } + } self.bytes.deinit(gpa); self.hash_table.deinit(gpa); } @@ -134,34 +169,46 @@ fn resizeHashTable(self: *SmallStringInterner, gpa: std.mem.Allocator) std.mem.A /// Add a string to this interner, returning a unique, serial index. pub fn insert(self: *SmallStringInterner, gpa: std.mem.Allocator, string: []const u8) std.mem.Allocator.Error!Idx { - if (std.debug.runtime_safety) { - std.debug.assert(!self.frozen); // Should not insert into a frozen interner - } - - // Check if we need to resize the hash table (when 80% full = entry_count * 5 >= hash_table.len() * 4) - if (self.entry_count * 5 >= self.hash_table.len() * 4) { - try self.resizeHashTable(gpa); - } - - // Find the string or the slot where it should be inserted + // First check if string exists const result = self.findStringOrSlot(string); if (result.idx) |existing_idx| { // String already exists return existing_idx; - } else { - // String doesn't exist, add it to bytes - const new_offset: Idx = @enumFromInt(self.bytes.len()); - - _ = try self.bytes.appendSlice(gpa, string); - _ = try self.bytes.append(gpa, 0); - - // Add to hash table - self.hash_table.items.items[@intCast(result.slot)] = new_offset; - self.entry_count += 1; - - return new_offset; } + + // Debug assertion: deserialized interners should never need new inserts. + // If this fires, it's a bug - all idents should already exist in the interner. + if (std.debug.runtime_safety) { + if (!self.supports_inserts) { + @panic("insert called on deserialized interner - this is a bug, ident should already exist"); + } + } + + // Check if resize needed. + if (self.entry_count * 5 >= self.hash_table.len() * 4) { + try self.resizeHashTable(gpa); + // After resize, need to find the slot again + const new_result = self.findStringOrSlot(string); + return self.insertAt(gpa, string, new_result.slot); + } + + // No resize needed, insert at the found slot + return self.insertAt(gpa, string, result.slot); +} + +/// Insert a string at a specific slot (internal helper). +fn insertAt(self: *SmallStringInterner, gpa: std.mem.Allocator, string: []const u8, slot: u64) std.mem.Allocator.Error!Idx { + const new_offset: Idx = @enumFromInt(self.bytes.len()); + + _ = try self.bytes.appendSlice(gpa, string); + _ = try self.bytes.append(gpa, 0); + + // Add to hash table + self.hash_table.items.items[@intCast(slot)] = new_offset; + self.entry_count += 1; + + return new_offset; } /// Check if a string is already interned in this interner, used for generating unique names. @@ -170,21 +217,21 @@ pub fn contains(self: *const SmallStringInterner, string: []const u8) bool { return result.idx != null; } +/// Look up a string in this interner and return its index if found. +/// Unlike insert, this never modifies the interner (no resize, no insertion). +/// Useful for deserialized interners that cannot be grown. +pub fn lookup(self: *const SmallStringInterner, string: []const u8) ?Idx { + const result = self.findStringOrSlot(string); + return result.idx; +} + /// Get a reference to the text for an interned string. pub fn getText(self: *const SmallStringInterner, idx: Idx) []u8 { const bytes_slice = self.bytes.items.items; const start = @intFromEnum(idx); - return std.mem.sliceTo(bytes_slice[start..], 0); } -/// Freeze the interner, preventing any new entries from being added. -pub fn freeze(self: *SmallStringInterner) void { - if (std.debug.runtime_safety) { - self.frozen = true; - } -} - /// Serialize this interner to the given CompactWriter. The resulting interner /// in the writer's buffer will have offsets instead of pointers. Calling any /// methods on it or dereferencing its internal "pointers" (which are now @@ -205,7 +252,6 @@ pub fn serialize( .bytes = serialized_bytes.*, .hash_table = serialized_hash_table.*, .entry_count = self.entry_count, - .frozen = self.frozen, }; // Return the version of Self that's in the writer's buffer @@ -219,11 +265,13 @@ pub fn relocate(self: *SmallStringInterner, offset: isize) void { } /// Serialized representation of a SmallStringInterner -pub const Serialized = struct { +/// Uses extern struct to guarantee consistent field layout across optimization levels. +pub const Serialized = extern struct { bytes: collections.SafeList(u8).Serialized, hash_table: collections.SafeList(Idx).Serialized, entry_count: u32, - frozen: if (std.debug.runtime_safety) bool else void, + /// Padding to maintain struct alignment + _padding: u32 = 0, /// Serialize a SmallStringInterner into this Serialized struct, appending data to the writer pub fn serialize( @@ -238,22 +286,37 @@ pub const Serialized = struct { try self.hash_table.serialize(&interner.hash_table, allocator, writer); // Copy simple values directly self.entry_count = interner.entry_count; - self.frozen = interner.frozen; + self._padding = 0; } /// Deserialize this Serialized struct into a SmallStringInterner pub fn deserialize(self: *Serialized, offset: i64) *SmallStringInterner { - // Self.Serialized should be at least as big as Self - std.debug.assert(@sizeOf(Serialized) >= @sizeOf(SmallStringInterner)); + // Verify that Serialized is at least as large as the runtime struct. + comptime { + if (@sizeOf(Serialized) < @sizeOf(SmallStringInterner)) { + @compileError(std.fmt.comptimePrint( + "SmallStringInterner.Serialized ({d} bytes) is smaller than SmallStringInterner ({d} bytes)", + .{ @sizeOf(Serialized), @sizeOf(SmallStringInterner) }, + )); + } + } // Overwrite ourself with the deserialized version, and return our pointer after casting it to Self. const interner = @as(*SmallStringInterner, @ptrCast(self)); + // Read values from Serialized BEFORE any writes (required for in-place deserialization) + const saved_entry_count = self.entry_count; + + // Now deserialize (which does in-place writes) + const bytes_val = self.bytes.deserialize(offset).*; + const hash_table_val = self.hash_table.deserialize(offset).*; + interner.* = .{ - .bytes = self.bytes.deserialize(offset).*, - .hash_table = self.hash_table.deserialize(offset).*, - .entry_count = self.entry_count, - .frozen = self.frozen, + .bytes = bytes_val, + .hash_table = hash_table_val, + .entry_count = saved_entry_count, + // Debug-only: mark as not supporting inserts - deserialized interners should never need new idents + .supports_inserts = if (std.debug.runtime_safety) false else {}, }; return interner; @@ -290,7 +353,7 @@ test "SmallStringInterner empty CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); _ = try file.read(buffer); @@ -326,12 +389,12 @@ test "SmallStringInterner basic CompactWriter roundtrip" { "duplicate", // Should reuse the same index }; - var indices = std.ArrayList(SmallStringInterner.Idx).init(gpa); - defer indices.deinit(); + var indices = std.ArrayList(SmallStringInterner.Idx).empty; + defer indices.deinit(gpa); for (test_strings) |str| { const idx = try original.insert(gpa, str); - try indices.append(idx); + try indices.append(gpa, idx); } // Verify duplicate detection worked @@ -360,7 +423,7 @@ test "SmallStringInterner basic CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); _ = try file.read(buffer); @@ -433,7 +496,7 @@ test "SmallStringInterner with populated hashmap CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); _ = try file.read(buffer); @@ -460,7 +523,7 @@ test "SmallStringInterner with populated hashmap CompactWriter roundtrip" { try std.testing.expect(original_entry_count > 0); } -test "SmallStringInterner frozen state CompactWriter roundtrip" { +test "SmallStringInterner CompactWriter roundtrip" { const gpa = std.testing.allocator; // Create and populate interner @@ -470,14 +533,6 @@ test "SmallStringInterner frozen state CompactWriter roundtrip" { _ = try original.insert(gpa, "test1"); _ = try original.insert(gpa, "test2"); - // Freeze the interner - original.freeze(); - - // Verify it's frozen - if (std.debug.runtime_safety) { - try std.testing.expect(original.frozen); - } - // Create a temp file var tmp_dir = std.testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -501,7 +556,7 @@ test "SmallStringInterner frozen state CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); _ = try file.read(buffer); @@ -510,11 +565,6 @@ test "SmallStringInterner frozen state CompactWriter roundtrip" { const deserialized = @as(*SmallStringInterner, @ptrCast(@alignCast(buffer.ptr))); deserialized.relocate(@as(isize, @intCast(@intFromPtr(buffer.ptr)))); - // Verify frozen state is preserved - if (std.debug.runtime_safety) { - try std.testing.expect(deserialized.frozen); - } - // Verify strings are still accessible // Note: Index 0 is reserved for the unused marker, so strings start at index 1 try std.testing.expectEqualStrings("test1", deserialized.getText(@enumFromInt(1))); @@ -541,12 +591,12 @@ test "SmallStringInterner edge cases CompactWriter roundtrip" { " start_with_space", }; - var indices = std.ArrayList(SmallStringInterner.Idx).init(gpa); - defer indices.deinit(); + var indices = std.ArrayList(SmallStringInterner.Idx).empty; + defer indices.deinit(gpa); for (edge_cases) |str| { const idx = try original.insert(gpa, str); - try indices.append(idx); + try indices.append(gpa, idx); } // Create a temp file @@ -572,7 +622,7 @@ test "SmallStringInterner edge cases CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); _ = try file.read(buffer); @@ -662,7 +712,7 @@ test "SmallStringInterner edge cases CompactWriter roundtrip" { // // Read back // try file.seekTo(0); // const file_size = try file.getEndPos(); -// const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); +// const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); // defer gpa.free(buffer); // _ = try file.read(buffer); @@ -682,14 +732,11 @@ test "SmallStringInterner edge cases CompactWriter roundtrip" { // try std.testing.expectEqualStrings("interner1_string2", deserialized1.getText(idx1_2)); // try std.testing.expectEqual(@as(u32, 2), deserialized1.entry_count); -// // Verify interner 2 (frozen) +// // Verify interner 2 // try std.testing.expectEqualStrings("interner2_string1", deserialized2.getText(idx2_1)); // try std.testing.expectEqualStrings("interner2_string2", deserialized2.getText(idx2_2)); // try std.testing.expectEqualStrings("interner2_string3", deserialized2.getText(idx2_3)); // try std.testing.expectEqual(@as(u32, 3), deserialized2.entry_count); -// if (std.debug.runtime_safety) { -// try std.testing.expect(deserialized2.frozen); -// } // // Verify interner 3 // try std.testing.expectEqualStrings("interner3_string1", deserialized3.getText(idx3_1)); diff --git a/src/base/StringLiteral.zig b/src/base/StringLiteral.zig index 079c583166..9c3da1c637 100644 --- a/src/base/StringLiteral.zig +++ b/src/base/StringLiteral.zig @@ -33,11 +33,6 @@ pub const Store = struct { /// the first 7 bit would signal the length, the last bit would signal that the length /// continues to the previous byte buffer: collections.SafeList(u8) = .{}, - /// When true, no new entries can be added to the store. - /// This is set after canonicalization is complete, so that - /// we know it's safe to serialize/deserialize the part of the interner - /// that goes from ident to string, because we don't go from string to ident anymore. - frozen: if (std.debug.runtime_safety) bool else void = if (std.debug.runtime_safety) false else {}, /// Intiizalizes a `Store` with capacity `bytes` of space. /// Note this specifically is the number of bytes for storing strings. @@ -57,9 +52,6 @@ pub const Store = struct { /// /// Does not deduplicate, as string literals are expected to be large and mostly unique. pub fn insert(self: *Store, gpa: std.mem.Allocator, string: []const u8) std.mem.Allocator.Error!Idx { - if (std.debug.runtime_safety) { - std.debug.assert(!self.frozen); // Should not insert into a frozen store - } const str_len: u32 = @truncate(string.len); const str_len_bytes = std.mem.asBytes(&str_len); @@ -79,13 +71,6 @@ pub const Store = struct { return self.buffer.items.items[idx_u32 .. idx_u32 + str_len]; } - /// Freeze the store, preventing any new entries from being added. - pub fn freeze(self: *Store) void { - if (std.debug.runtime_safety) { - self.frozen = true; - } - } - /// Serialize this Store to the given CompactWriter. The resulting Store /// in the writer's buffer will have offsets instead of pointers. Calling any /// methods on it or dereferencing its internal "pointers" (which are now @@ -101,7 +86,6 @@ pub const Store = struct { // Then serialize the buffer SafeList and update the struct offset_self.* = .{ .buffer = (try self.buffer.serialize(allocator, writer)).*, - .frozen = self.frozen, }; return @constCast(offset_self); @@ -113,9 +97,9 @@ pub const Store = struct { } /// Serialized representation of a Store - pub const Serialized = struct { + /// Uses extern struct to guarantee consistent field layout across optimization levels. + pub const Serialized = extern struct { buffer: collections.SafeList(u8).Serialized, - frozen: if (std.debug.runtime_safety) bool else void, /// Serialize a Store into this Serialized struct, appending data to the writer pub fn serialize( @@ -126,21 +110,15 @@ pub const Store = struct { ) std.mem.Allocator.Error!void { // Serialize the buffer SafeList try self.buffer.serialize(&store.buffer, allocator, writer); - // Copy the frozen field - self.frozen = store.frozen; } /// Deserialize this Serialized struct into a Store pub fn deserialize(self: *Serialized, offset: i64) *Store { - // Store.Serialized should be at least as big as Store - std.debug.assert(@sizeOf(Serialized) >= @sizeOf(Store)); - // Overwrite ourself with the deserialized version, and return our pointer after casting it to Self. const store = @as(*Store, @ptrFromInt(@intFromPtr(self))); store.* = Store{ .buffer = self.buffer.deserialize(offset).*, - .frozen = self.frozen, }; return store; @@ -193,7 +171,7 @@ test "Store empty CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); _ = try file.read(buffer); @@ -245,7 +223,7 @@ test "Store basic CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); _ = try file.read(buffer); @@ -280,12 +258,12 @@ test "Store comprehensive CompactWriter roundtrip" { "very long string " ** 50, // long string }; - var indices = std.ArrayList(Idx).init(gpa); - defer indices.deinit(); + var indices = std.ArrayList(Idx).empty; + defer indices.deinit(gpa); for (test_strings) |str| { const idx = try original.insert(gpa, str); - try indices.append(idx); + try indices.append(gpa, idx); } // Create a temp file @@ -311,7 +289,7 @@ test "Store comprehensive CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); _ = try file.read(buffer); @@ -328,7 +306,7 @@ test "Store comprehensive CompactWriter roundtrip" { } } -test "Store frozen state CompactWriter roundtrip" { +test "Store CompactWriter roundtrip" { const gpa = std.testing.allocator; // Create and populate store @@ -338,14 +316,6 @@ test "Store frozen state CompactWriter roundtrip" { _ = try original.insert(gpa, "test1"); _ = try original.insert(gpa, "test2"); - // Freeze the store - original.freeze(); - - // Verify store is frozen - if (std.debug.runtime_safety) { - try std.testing.expect(original.frozen); - } - // Create a temp file var tmp_dir = std.testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -369,7 +339,7 @@ test "Store frozen state CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); _ = try file.read(buffer); @@ -377,11 +347,6 @@ test "Store frozen state CompactWriter roundtrip" { // Cast and relocate const deserialized = @as(*Store, @ptrCast(@alignCast(buffer.ptr))); deserialized.relocate(@as(isize, @intCast(@intFromPtr(buffer.ptr)))); - - // Verify frozen state is preserved - if (std.debug.runtime_safety) { - try std.testing.expect(deserialized.frozen); - } } test "Store.Serialized roundtrip" { @@ -395,9 +360,6 @@ test "Store.Serialized roundtrip" { const idx2 = try original.insert(gpa, "world"); const idx3 = try original.insert(gpa, "foo bar baz"); - // Freeze the store in debug mode - original.freeze(); - // Create a CompactWriter and arena var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); @@ -432,11 +394,6 @@ test "Store.Serialized roundtrip" { try std.testing.expectEqualStrings("hello", store.get(idx1)); try std.testing.expectEqualStrings("world", store.get(idx2)); try std.testing.expectEqualStrings("foo bar baz", store.get(idx3)); - - // Verify frozen state is preserved - if (std.debug.runtime_safety) { - try std.testing.expect(store.frozen); - } } test "Store edge case indices CompactWriter roundtrip" { @@ -489,7 +446,7 @@ test "Store edge case indices CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @as(usize, @intCast(file_size))); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @as(usize, @intCast(file_size))); defer gpa.free(buffer); _ = try file.read(buffer); diff --git a/src/base/mod.zig b/src/base/mod.zig index ec3eb65514..eadcae68c1 100644 --- a/src/base/mod.zig +++ b/src/base/mod.zig @@ -11,14 +11,16 @@ pub const parallel = @import("parallel.zig"); pub const SmallStringInterner = @import("SmallStringInterner.zig"); pub const safe_memory = @import("safe_memory.zig"); +pub const stack_overflow = @import("stack_overflow.zig"); pub const target = @import("target.zig"); -pub const DataSpan = @import("DataSpan.zig"); +pub const DataSpan = @import("DataSpan.zig").DataSpan; pub const PackedDataSpan = @import("PackedDataSpan.zig").PackedDataSpan; pub const FunctionArgs = @import("PackedDataSpan.zig").FunctionArgs; pub const SmallCollections = @import("PackedDataSpan.zig").SmallCollections; pub const CommonEnv = @import("CommonEnv.zig"); +pub const source_utils = @import("source_utils.zig"); test { _ = @import("Ident.zig"); @@ -46,14 +48,14 @@ pub const CalledVia = enum { string_interpolation, /// This call is the result of desugaring a map2-based Record Builder field. e.g. /// ```roc - /// { Result.parallel <- + /// { Try.parallel <- /// foo: get("a"), /// bar: get("b"), /// } /// ``` /// is transformed into /// ```roc - /// Result.parallel(get("a"), get("b"), (|foo, bar | { foo, bar })) + /// Try.parallel(get("a"), get("b"), (|foo, bar | { foo, bar })) /// ``` record_builder, }; @@ -92,11 +94,60 @@ pub const FracLiteral = union(enum) { }; /// An integer or fractional number literal. -pub const NumLiteral = union(enum) { +pub const Numeral = union(enum) { Int: IntLiteral, Frac: FracLiteral, }; +/// The core allocators for the lifetime of a roc program. +/// +/// This structure should be used to pass allocators to most functions in Roc. +/// Data structures should anchor to a generic allocator instead (alloc: Allocator). +/// It is up to the instanciator of the data structure to pick what it will use. +/// Generally speaking though, data structures can realloc and will use the gpa. +/// +/// IMPORTANT: After initialization, Allocators must always be passed by pointer (*Allocators), +/// never by value. Passing by value will invalidate the arena allocator pointer! +pub const Allocators = struct { + /// The gpa is the general purpose allocator. Anything allocated with the gpa must be freed. + /// the gpa should generally be used for large allocations and things that might get reallocated. + /// It is best to avoid allocating small or short lived things with the gpa. + gpa: std.mem.Allocator, + + /// The arena is an arena allocator that is around for the entire roc compilation. + /// The arena should be used for small and miscellaneous allocations. + /// Things allocated in arena are expected to never be freed individually. + /// + /// IMPORTANT: This field contains a pointer to arena_impl. The struct must not be + /// moved after initialization, or this pointer will be invalidated. + arena: std.mem.Allocator, + + /// The underlying arena allocator implementation (stored to enable deinit) + arena_impl: std.heap.ArenaAllocator, + + // TODO: consider if we want to add scratch. It would be an arena reset between each compilation phase. + // scratch: ?std.mem.Allocator, + + /// Initialize the Allocators in-place with a general purpose allocator. + /// + /// IMPORTANT: This struct must be initialized in its final memory location. + /// After calling initInPlace(), the struct must only be passed by pointer (*Allocators), + /// never by value, or the arena allocator pointer will be invalidated. + pub fn initInPlace(self: *Allocators, gpa: std.mem.Allocator) void { + self.* = .{ + .gpa = gpa, + .arena = undefined, + .arena_impl = std.heap.ArenaAllocator.init(gpa), + }; + self.arena = self.arena_impl.allocator(); + } + + /// Deinitialize the arena allocator. + pub fn deinit(self: *Allocators) void { + self.arena_impl.deinit(); + } +}; + test "base tests" { std.testing.refAllDecls(@import("CommonEnv.zig")); std.testing.refAllDecls(@import("DataSpan.zig")); @@ -109,6 +160,8 @@ test "base tests" { std.testing.refAllDecls(@import("Scratch.zig")); std.testing.refAllDecls(@import("SExprTree.zig")); std.testing.refAllDecls(@import("SmallStringInterner.zig")); + std.testing.refAllDecls(@import("source_utils.zig")); + std.testing.refAllDecls(@import("stack_overflow.zig")); std.testing.refAllDecls(@import("StringLiteral.zig")); std.testing.refAllDecls(@import("target.zig")); } diff --git a/src/base/parallel.zig b/src/base/parallel.zig index 4db8135e54..6dceb1c837 100644 --- a/src/base/parallel.zig +++ b/src/base/parallel.zig @@ -1,10 +1,17 @@ //! Parallel processing utilities and thread management for the Roc compiler. //! //! (Currently only used in the snapshot tool) +//! +//! For wasm32-freestanding: Threading is not available, so multi-threaded +//! processing falls back to single-threaded execution. const std = @import("std"); +const builtin = @import("builtin"); const Allocator = std.mem.Allocator; -const Thread = std.Thread; + +// Threading is not available on freestanding targets +const is_freestanding = builtin.os.tag == .freestanding; +const Thread = if (is_freestanding) void else std.Thread; /// Atomic type for thread-safe usize operations pub const AtomicUsize = std.atomic.Value(usize); @@ -90,7 +97,10 @@ pub fn process( return; } - if (options.max_threads == 1) { + // For freestanding targets, always use single-threaded processing + const effective_max_threads = if (is_freestanding) 1 else options.max_threads; + + if (effective_max_threads == 1) { // Process everything in main thread var index = AtomicUsize.init(0); const ctx = WorkerContext(T){ @@ -103,18 +113,23 @@ pub fn process( }; workerThread(T, ctx); } else { + // Multi-threaded path (only available on non-freestanding targets) + if (comptime is_freestanding) { + unreachable; // Should not reach here due to effective_max_threads check above + } + const thread_count = @min( - if (options.max_threads == 0) std.Thread.getCpuCount() catch 1 else options.max_threads, + if (effective_max_threads == 0) std.Thread.getCpuCount() catch 1 else effective_max_threads, work_item_count, ); var index = AtomicUsize.init(0); const fixed_stack_thread_count: usize = 16; var threads: [fixed_stack_thread_count]Thread = undefined; - var extra_threads: std.ArrayList(Thread) = undefined; + var extra_threads: std.array_list.Managed(Thread) = undefined; if (thread_count > fixed_stack_thread_count) { - extra_threads = std.ArrayList(Thread).init(allocator); + extra_threads = std.array_list.Managed(Thread).init(allocator); } // Start worker threads diff --git a/src/base/safe_memory.zig b/src/base/safe_memory.zig index 8623dec262..dd3bce8263 100644 --- a/src/base/safe_memory.zig +++ b/src/base/safe_memory.zig @@ -106,9 +106,8 @@ test "safeCast and safeRead" { var buffer = [_]u8{ 0x12, 0x34, 0x56, 0x78 }; const ptr = @as(*anyopaque, @ptrCast(&buffer)); - const value = try safeRead(u16, ptr, 0, 4); - // Endianness dependent, but should not crash - _ = value; + // Just verify this doesn't error - actual value is endianness dependent + _ = try safeRead(u16, ptr, 0, 4); try std.testing.expectError(error.BufferOverflow, safeRead(u32, ptr, 1, 4)); } diff --git a/src/base/source_utils.zig b/src/base/source_utils.zig new file mode 100644 index 0000000000..13db8b9a21 --- /dev/null +++ b/src/base/source_utils.zig @@ -0,0 +1,271 @@ +//! Utility functions for processing Roc source code. + +const std = @import("std"); + +/// Normalizes line endings in source code by converting CRLF (\r\n) to LF (\n). +/// +/// This ensures consistent behavior across different operating systems. On Windows, +/// text files often have CRLF line endings, but Roc source code should be processed +/// with LF-only line endings for consistent parsing and formatting. +/// +/// The normalization is done in-place, modifying the input buffer and returning +/// a slice of the normalized content. The returned slice will be the same length +/// or shorter than the input. +/// +/// IMPORTANT: This function returns a sub-slice of the input. If the input was +/// allocated, the caller must keep track of the original allocation for freeing. +/// For allocated buffers where proper memory management is needed, use +/// `normalizeLineEndingsRealloc` instead. +/// +/// Standalone \r characters (not followed by \n) are preserved as-is - the tokenizer +/// will report these as errors separately via the MisplacedCarriageReturn diagnostic. +pub fn normalizeLineEndings(source: []u8) []u8 { + if (source.len == 0) return source; + + var write_pos: usize = 0; + var read_pos: usize = 0; + + while (read_pos < source.len) { + const c = source[read_pos]; + if (c == '\r' and read_pos + 1 < source.len and source[read_pos + 1] == '\n') { + // Skip the \r in \r\n sequence, only write the \n + read_pos += 1; + } else { + source[write_pos] = c; + write_pos += 1; + read_pos += 1; + } + } + + return source[0..write_pos]; +} + +/// Normalizes line endings and reallocates the buffer to the correct size. +/// +/// This function normalizes CRLF to LF and properly handles memory: +/// - If no normalization is needed, returns the original buffer unchanged +/// - If normalization is needed, reallocates to the correct size and frees the original +/// +/// The returned buffer is always properly sized for freeing with the allocator. +/// The caller is responsible for freeing the returned buffer. +pub fn normalizeLineEndingsRealloc(allocator: std.mem.Allocator, source: []u8) std.mem.Allocator.Error![]u8 { + if (source.len == 0) return source; + + // First, check if normalization is needed and count CRLF sequences + var crlf_count: usize = 0; + for (0..source.len) |i| { + if (source[i] == '\r' and i + 1 < source.len and source[i + 1] == '\n') { + crlf_count += 1; + } + } + + // If no CRLF sequences, return original buffer unchanged + if (crlf_count == 0) { + return source; + } + + // Normalize in place first + var write_pos: usize = 0; + var read_pos: usize = 0; + + while (read_pos < source.len) { + const c = source[read_pos]; + if (c == '\r' and read_pos + 1 < source.len and source[read_pos + 1] == '\n') { + // Skip the \r in \r\n sequence, only write the \n + read_pos += 1; + } else { + source[write_pos] = c; + write_pos += 1; + read_pos += 1; + } + } + + // Allocate a new properly-sized buffer + const new_len = write_pos; + const new_buffer = try allocator.alloc(u8, new_len); + @memcpy(new_buffer, source[0..new_len]); + + // Free the original oversized buffer + allocator.free(source); + + return new_buffer; +} + +/// Normalizes line endings by allocating a new buffer if needed. +/// Returns the normalized source and a boolean indicating whether a new buffer was allocated. +/// If no normalization was needed, returns the original slice and false. +/// If normalization was performed, returns a new allocated slice and true. +/// +/// The caller is responsible for freeing the returned slice if the boolean is true. +pub fn normalizeLineEndingsAlloc(allocator: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error!struct { data: []u8, allocated: bool } { + // First pass: check if normalization is needed + var needs_normalization = false; + for (0..source.len) |i| { + if (source[i] == '\r' and i + 1 < source.len and source[i + 1] == '\n') { + needs_normalization = true; + break; + } + } + + if (!needs_normalization) { + // No CRLF sequences found, can use original buffer + // But we need to return a mutable copy since the caller expects []u8 + const copy = try allocator.alloc(u8, source.len); + @memcpy(copy, source); + return .{ .data = copy, .allocated = true }; + } + + // Count how many \r\n sequences there are to calculate exact output size + var crlf_count: usize = 0; + for (0..source.len) |i| { + if (source[i] == '\r' and i + 1 < source.len and source[i + 1] == '\n') { + crlf_count += 1; + } + } + + // Allocate exact size needed + const new_len = source.len - crlf_count; + const result = try allocator.alloc(u8, new_len); + + // Copy with normalization + var write_pos: usize = 0; + var read_pos: usize = 0; + while (read_pos < source.len) { + const c = source[read_pos]; + if (c == '\r' and read_pos + 1 < source.len and source[read_pos + 1] == '\n') { + // Skip the \r in \r\n sequence + read_pos += 1; + } else { + result[write_pos] = c; + write_pos += 1; + read_pos += 1; + } + } + + return .{ .data = result, .allocated = true }; +} + +test "normalizeLineEndings - no changes needed" { + const allocator = std.testing.allocator; + + // Test with LF-only content + { + const source = try allocator.dupe(u8, "hello\nworld\n"); + defer allocator.free(source); + const result = normalizeLineEndings(source); + try std.testing.expectEqualStrings("hello\nworld\n", result); + } + + // Test with empty content + { + const source: []u8 = &.{}; + const result = normalizeLineEndings(source); + try std.testing.expectEqualStrings("", result); + } +} + +test "normalizeLineEndings - CRLF to LF" { + const allocator = std.testing.allocator; + + // Test with CRLF content + { + const source = try allocator.dupe(u8, "hello\r\nworld\r\n"); + defer allocator.free(source); + const result = normalizeLineEndings(source); + try std.testing.expectEqualStrings("hello\nworld\n", result); + } + + // Test with mixed line endings + { + const source = try allocator.dupe(u8, "line1\r\nline2\nline3\r\n"); + defer allocator.free(source); + const result = normalizeLineEndings(source); + try std.testing.expectEqualStrings("line1\nline2\nline3\n", result); + } +} + +test "normalizeLineEndings - standalone CR preserved" { + const allocator = std.testing.allocator; + + // Standalone \r (not followed by \n) should be preserved + // The tokenizer will handle these as errors + { + const source = try allocator.dupe(u8, "hello\rworld"); + defer allocator.free(source); + const result = normalizeLineEndings(source); + try std.testing.expectEqualStrings("hello\rworld", result); + } + + // Mix of standalone \r and \r\n + { + const source = try allocator.dupe(u8, "a\rb\r\nc"); + defer allocator.free(source); + const result = normalizeLineEndings(source); + try std.testing.expectEqualStrings("a\rb\nc", result); + } +} + +test "normalizeLineEndings - multiline strings" { + const allocator = std.testing.allocator; + + // Test with Roc multiline string syntax + { + const source = try allocator.dupe(u8, "lines =\r\n \\\\first line\r\nOk(lines)\r\n"); + defer allocator.free(source); + const result = normalizeLineEndings(source); + try std.testing.expectEqualStrings("lines =\n \\\\first line\nOk(lines)\n", result); + } +} + +test "normalizeLineEndingsAlloc - allocates new buffer" { + const allocator = std.testing.allocator; + + // Test with CRLF content - should allocate new buffer + { + const source = "hello\r\nworld\r\n"; + const result = try normalizeLineEndingsAlloc(allocator, source); + defer allocator.free(result.data); + try std.testing.expect(result.allocated); + try std.testing.expectEqualStrings("hello\nworld\n", result.data); + } + + // Test with LF-only content - still allocates a copy + { + const source = "hello\nworld\n"; + const result = try normalizeLineEndingsAlloc(allocator, source); + defer allocator.free(result.data); + try std.testing.expect(result.allocated); + try std.testing.expectEqualStrings("hello\nworld\n", result.data); + } +} + +test "normalizeLineEndingsRealloc - proper memory management" { + const allocator = std.testing.allocator; + + // Test with CRLF content - should reallocate to smaller buffer + { + const source = try allocator.dupe(u8, "hello\r\nworld\r\n"); + // source is now owned, normalizeLineEndingsRealloc will free it if needed + const result = try normalizeLineEndingsRealloc(allocator, source); + defer allocator.free(result); + try std.testing.expectEqualStrings("hello\nworld\n", result); + } + + // Test with LF-only content - should return original buffer unchanged + { + const source = try allocator.dupe(u8, "hello\nworld\n"); + const result = try normalizeLineEndingsRealloc(allocator, source); + defer allocator.free(result); + try std.testing.expectEqualStrings("hello\nworld\n", result); + // result should be the same pointer as source (no reallocation) + try std.testing.expectEqual(source.ptr, result.ptr); + } + + // Test with empty content + { + const source: []u8 = try allocator.alloc(u8, 0); + const result = try normalizeLineEndingsRealloc(allocator, source); + defer allocator.free(result); + try std.testing.expectEqualStrings("", result); + } +} diff --git a/src/base/stack_overflow.zig b/src/base/stack_overflow.zig new file mode 100644 index 0000000000..a4c2b361f4 --- /dev/null +++ b/src/base/stack_overflow.zig @@ -0,0 +1,263 @@ +//! Signal handling for the Roc compiler (stack overflow, segfault, division by zero). +//! +//! This module provides a thin wrapper around the generic signal handlers in +//! builtins.handlers, configured with compiler-specific error messages. +//! +//! On POSIX systems (Linux, macOS), we use sigaltstack to set up an alternate +//! signal stack and install handlers for SIGSEGV, SIGBUS, and SIGFPE. +//! +//! On Windows, we use SetUnhandledExceptionFilter to catch various exceptions. +//! +//! Freestanding targets (like wasm32) are not supported (no signal handling available). + +const std = @import("std"); +const builtin = @import("builtin"); +const handlers = @import("builtins").handlers; +const posix = if (builtin.os.tag != .windows and builtin.os.tag != .freestanding) std.posix else undefined; + +/// Error message to display on stack overflow +const STACK_OVERFLOW_MESSAGE = "\nThe Roc compiler overflowed its stack memory and had to exit.\n\n"; + +/// Callback for stack overflow in the compiler +fn handleStackOverflow() noreturn { + if (comptime builtin.os.tag == .windows) { + // Windows: use WriteFile for signal-safe output + const DWORD = u32; + const HANDLE = ?*anyopaque; + const STD_ERROR_HANDLE: DWORD = @bitCast(@as(i32, -12)); + + const kernel32 = struct { + extern "kernel32" fn GetStdHandle(nStdHandle: DWORD) callconv(.winapi) HANDLE; + extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*anyopaque) callconv(.winapi) i32; + extern "kernel32" fn ExitProcess(uExitCode: c_uint) callconv(.winapi) noreturn; + }; + + const stderr_handle = kernel32.GetStdHandle(STD_ERROR_HANDLE); + var bytes_written: DWORD = 0; + _ = kernel32.WriteFile(stderr_handle, STACK_OVERFLOW_MESSAGE.ptr, STACK_OVERFLOW_MESSAGE.len, &bytes_written, null); + kernel32.ExitProcess(134); + } else if (comptime builtin.os.tag != .freestanding) { + // POSIX: use direct write syscall for signal-safety + _ = posix.write(posix.STDERR_FILENO, STACK_OVERFLOW_MESSAGE) catch {}; + posix.exit(134); + } else { + // WASI fallback + std.process.exit(134); + } +} + +/// Error message to display on arithmetic error (division by zero, etc.) +const ARITHMETIC_ERROR_MESSAGE = "\nThe Roc compiler divided by zero and had to exit.\n\n"; + +/// Callback for arithmetic errors (division by zero) in the compiler +fn handleArithmeticError() noreturn { + if (comptime builtin.os.tag == .windows) { + const DWORD = u32; + const HANDLE = ?*anyopaque; + const STD_ERROR_HANDLE: DWORD = @bitCast(@as(i32, -12)); + + const kernel32 = struct { + extern "kernel32" fn GetStdHandle(nStdHandle: DWORD) callconv(.winapi) HANDLE; + extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*anyopaque) callconv(.winapi) i32; + extern "kernel32" fn ExitProcess(uExitCode: c_uint) callconv(.winapi) noreturn; + }; + + const stderr_handle = kernel32.GetStdHandle(STD_ERROR_HANDLE); + var bytes_written: DWORD = 0; + _ = kernel32.WriteFile(stderr_handle, ARITHMETIC_ERROR_MESSAGE.ptr, ARITHMETIC_ERROR_MESSAGE.len, &bytes_written, null); + kernel32.ExitProcess(136); + } else if (comptime builtin.os.tag != .freestanding) { + _ = posix.write(posix.STDERR_FILENO, ARITHMETIC_ERROR_MESSAGE) catch {}; + posix.exit(136); // 128 + 8 (SIGFPE) + } else { + std.process.exit(136); + } +} + +/// Callback for access violation in the compiler +fn handleAccessViolation(fault_addr: usize) noreturn { + if (comptime builtin.os.tag == .windows) { + const DWORD = u32; + const HANDLE = ?*anyopaque; + const STD_ERROR_HANDLE: DWORD = @bitCast(@as(i32, -12)); + + const kernel32 = struct { + extern "kernel32" fn GetStdHandle(nStdHandle: DWORD) callconv(.winapi) HANDLE; + extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*anyopaque) callconv(.winapi) i32; + extern "kernel32" fn ExitProcess(uExitCode: c_uint) callconv(.winapi) noreturn; + }; + + var addr_buf: [18]u8 = undefined; + const addr_str = handlers.formatHex(fault_addr, &addr_buf); + + const msg1 = "\nAccess violation in the Roc compiler.\nFault address: "; + const msg2 = "\n\nPlease report this issue at: https://github.com/roc-lang/roc/issues\n\n"; + const stderr_handle = kernel32.GetStdHandle(STD_ERROR_HANDLE); + var bytes_written: DWORD = 0; + _ = kernel32.WriteFile(stderr_handle, msg1.ptr, msg1.len, &bytes_written, null); + _ = kernel32.WriteFile(stderr_handle, addr_str.ptr, @intCast(addr_str.len), &bytes_written, null); + _ = kernel32.WriteFile(stderr_handle, msg2.ptr, msg2.len, &bytes_written, null); + kernel32.ExitProcess(139); + } else { + // POSIX (and WASI fallback): use direct write syscall for signal-safety + const generic_msg = "\nSegmentation fault (SIGSEGV) in the Roc compiler.\nFault address: "; + _ = posix.write(posix.STDERR_FILENO, generic_msg) catch {}; + + // Write the fault address as hex + var addr_buf: [18]u8 = undefined; + const addr_str = handlers.formatHex(fault_addr, &addr_buf); + _ = posix.write(posix.STDERR_FILENO, addr_str) catch {}; + _ = posix.write(posix.STDERR_FILENO, "\n\nPlease report this issue at: https://github.com/roc-lang/roc/issues\n\n") catch {}; + posix.exit(139); + } +} + +/// Install signal handlers for stack overflow, segfault, and division by zero. +/// This should be called early in main() before any significant work is done. +/// Returns true if the handlers were installed successfully, false otherwise. +pub fn install() bool { + return handlers.install(handleStackOverflow, handleAccessViolation, handleArithmeticError); +} + +/// Test function that intentionally causes a stack overflow. +/// This is used to verify the handler works correctly. +pub fn triggerStackOverflowForTest() noreturn { + // Use a recursive function that can't be tail-call optimized + const S = struct { + fn recurse(n: usize) usize { + // Prevent tail-call optimization by doing work after the recursive call + var buf: [1024]u8 = undefined; + buf[0] = @truncate(n); + const result = if (n == 0) 0 else recurse(n + 1); + // Use the buffer to prevent it from being optimized away + return result + buf[0]; + } + }; + + // This will recurse until stack overflow + const result = S.recurse(1); + + // This should never be reached + std.debug.print("Unexpected result: {}\n", .{result}); + std.process.exit(1); +} + +test "formatHex" { + var buf: [18]u8 = undefined; + + const zero = handlers.formatHex(0, &buf); + try std.testing.expectEqualStrings("0x0", zero); + + const small = handlers.formatHex(0xff, &buf); + try std.testing.expectEqualStrings("0xff", small); + + const medium = handlers.formatHex(0xdeadbeef, &buf); + try std.testing.expectEqualStrings("0xdeadbeef", medium); +} + +/// Check if we're being run as a subprocess to trigger stack overflow. +/// This is called by tests to create a child process that will crash. +/// Returns true if we should trigger the overflow (and not return). +pub fn checkAndTriggerIfSubprocess() bool { + // Check for the special environment variable that signals we should crash + const env_val = std.process.getEnvVarOwned(std.heap.page_allocator, "ROC_TEST_TRIGGER_STACK_OVERFLOW") catch return false; + defer std.heap.page_allocator.free(env_val); + + if (std.mem.eql(u8, env_val, "1")) { + // Install handler and trigger overflow + _ = install(); + triggerStackOverflowForTest(); + // Never returns + } + return false; +} + +test "stack overflow handler produces helpful error message" { + // Skip on freestanding targets - no process spawning or signal handling + if (comptime builtin.os.tag == .freestanding) { + return error.SkipZigTest; + } + + if (comptime builtin.os.tag == .windows) { + // Windows test would need subprocess spawning which is more complex + // The handler is installed and works, but testing it is harder + // For now, just verify the handler installs successfully + if (install()) { + return; // Success - handler installed + } + return error.SkipZigTest; + } + + try testStackOverflowPosix(); +} + +fn testStackOverflowPosix() !void { + // Create a pipe to capture stderr from the child + const pipe_fds = try posix.pipe(); + const pipe_read = pipe_fds[0]; + const pipe_write = pipe_fds[1]; + + const fork_result = posix.fork() catch { + posix.close(pipe_read); + posix.close(pipe_write); + return error.ForkFailed; + }; + + if (fork_result == 0) { + // Child process + posix.close(pipe_read); + + // Redirect stderr to the pipe + posix.dup2(pipe_write, posix.STDERR_FILENO) catch posix.exit(99); + posix.close(pipe_write); + + // Install the handler and trigger stack overflow + _ = install(); + triggerStackOverflowForTest(); + // Should never reach here + unreachable; + } else { + // Parent process + posix.close(pipe_write); + + // Wait for child to exit + const wait_result = posix.waitpid(fork_result, 0); + const status = wait_result.status; + + // Parse the wait status (Unix encoding) + const exited_normally = (status & 0x7f) == 0; + const exit_code: u8 = @truncate((status >> 8) & 0xff); + const termination_signal: u8 = @truncate(status & 0x7f); + + // Read stderr output from child + var stderr_buf: [4096]u8 = undefined; + const bytes_read = posix.read(pipe_read, &stderr_buf) catch 0; + posix.close(pipe_read); + + const stderr_output = stderr_buf[0..bytes_read]; + + try verifyHandlerOutput(exited_normally, exit_code, termination_signal, stderr_output); + } +} + +fn verifyHandlerOutput(exited_normally: bool, exit_code: u8, termination_signal: u8, stderr_output: []const u8) !void { + // Exit code 134 = stack overflow detected + // Exit code 139 = generic segfault (handler caught it but didn't classify as stack overflow) + if (exited_normally and (exit_code == 134 or exit_code == 139)) { + // Check that our handler message was printed + const has_stack_overflow_msg = std.mem.indexOf(u8, stderr_output, "overflowed its stack memory") != null; + const has_segfault_msg = std.mem.indexOf(u8, stderr_output, "Segmentation fault") != null; + + // Handler should have printed EITHER stack overflow message OR segfault message + try std.testing.expect(has_stack_overflow_msg or has_segfault_msg); + } else if (!exited_normally and (termination_signal == posix.SIG.SEGV or termination_signal == posix.SIG.BUS)) { + // The handler might not have caught it - this can happen on some systems + // where the signal delivery is different. Just warn and skip. + std.debug.print("Warning: Stack overflow was not caught by handler (signal {})\n", .{termination_signal}); + return error.SkipZigTest; + } else { + std.debug.print("Unexpected exit status: exited={}, code={}, signal={}\n", .{ exited_normally, exit_code, termination_signal }); + std.debug.print("Stderr: {s}\n", .{stderr_output}); + return error.TestUnexpectedResult; + } +} diff --git a/src/base58/mod.zig b/src/base58/mod.zig index 05ee8ad3c7..2fd82e0154 100644 --- a/src/base58/mod.zig +++ b/src/base58/mod.zig @@ -13,5 +13,5 @@ pub const encode = base58.encode; pub const decode = base58.decode; test { - _ = base58; + @import("std").testing.refAllDecls(@This()); } diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig new file mode 100644 index 0000000000..d936351bdd --- /dev/null +++ b/src/build/builtin_compiler/main.zig @@ -0,0 +1,1915 @@ +//! Build-time compiler for Roc builtin module (Builtin.roc). +//! +//! This executable runs during `zig build` on the host machine to: +//! 1. Parse and type-check the Builtin.roc module (which contains nested Bool, Try, Str, Dict, Set types) +//! 2. Serialize the resulting ModuleEnv to a binary file +//! 3. Output Builtin.bin to zig-out/builtins/ (which gets embedded in the roc binary) + +const std = @import("std"); +const base = @import("base"); +const parse = @import("parse"); +const can = @import("can"); +const check = @import("check"); +const collections = @import("collections"); +const types = @import("types"); +const reporting = @import("reporting"); + +const ModuleEnv = can.ModuleEnv; +const Can = can.Can; +const Check = check.Check; +const Allocator = std.mem.Allocator; +const CIR = can.CIR; +const Content = types.Content; +const Var = types.Var; + +const max_builtin_bytes = 1024 * 1024; + +// Stderr writer for diagnostic reporting +var stderr_buffer: [4096]u8 = undefined; +var stderr_writer: std.fs.File.Writer = undefined; +var stderr_initialized = false; + +fn stderrWriter() *std.Io.Writer { + if (!stderr_initialized) { + stderr_writer = std.fs.File.stderr().writer(&stderr_buffer); + stderr_initialized = true; + } + return &stderr_writer.interface; +} + +fn flushStderr() void { + if (stderr_initialized) { + stderr_writer.interface.flush() catch {}; + } +} + +// Use the canonical BuiltinIndices from CIR +const BuiltinIndices = CIR.BuiltinIndices; + +/// Replace specific e_anno_only expressions with e_low_level_lambda operations. +/// This transforms standalone annotations into low-level builtin lambda operations +/// that will be recognized by the compiler backend. +/// Returns a list of new def indices created. +fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) { + const gpa = env.gpa; + var new_def_indices = std.ArrayList(CIR.Def.Idx).empty; + + // Ensure types array has entries for all existing nodes + // This is necessary because varFrom(node_idx) assumes type_var index == node index + const current_nodes = env.store.nodes.len(); + const current_types = env.types.len(); + if (current_types < current_nodes) { + // Fill the gap with fresh type variables + var i: u64 = current_types; + while (i < current_nodes) : (i += 1) { + _ = env.types.fresh() catch unreachable; + } + } + + // Build a hashmap of (qualified name -> low-level operation) + var low_level_map = std.AutoHashMap(base.Ident.Idx, CIR.Expr.LowLevel).init(gpa); + defer low_level_map.deinit(); + + // Add all low-level operations to the map using full qualified names + // Associated items are stored as defs with qualified names like "Builtin.Str.is_empty" + // We need to find the actual ident that was created during canonicalization + if (env.common.findIdent("Builtin.Str.is_empty")) |str_is_empty_ident| { + try low_level_map.put(str_is_empty_ident, .str_is_empty); + } + if (env.common.findIdent("Builtin.Str.is_eq")) |str_is_eq_ident| { + try low_level_map.put(str_is_eq_ident, .str_is_eq); + } + if (env.common.findIdent("Builtin.Str.concat")) |str_concat_ident| { + try low_level_map.put(str_concat_ident, .str_concat); + } + if (env.common.findIdent("Builtin.Str.contains")) |str_contains_ident| { + try low_level_map.put(str_contains_ident, .str_contains); + } + if (env.common.findIdent("Builtin.Str.trim")) |str_trim_ident| { + try low_level_map.put(str_trim_ident, .str_trim); + } + if (env.common.findIdent("Builtin.Str.trim_start")) |str_trim_start_ident| { + try low_level_map.put(str_trim_start_ident, .str_trim_start); + } + if (env.common.findIdent("Builtin.Str.trim_end")) |str_trim_end_ident| { + try low_level_map.put(str_trim_end_ident, .str_trim_end); + } + if (env.common.findIdent("Builtin.Str.caseless_ascii_equals")) |str_caseless_ascii_equals_ident| { + try low_level_map.put(str_caseless_ascii_equals_ident, .str_caseless_ascii_equals); + } + if (env.common.findIdent("Builtin.Str.with_ascii_lowercased")) |str_with_ascii_lowercased_ident| { + try low_level_map.put(str_with_ascii_lowercased_ident, .str_with_ascii_lowercased); + } + if (env.common.findIdent("Builtin.Str.with_ascii_uppercased")) |str_with_ascii_uppercased_ident| { + try low_level_map.put(str_with_ascii_uppercased_ident, .str_with_ascii_uppercased); + } + if (env.common.findIdent("Builtin.Str.starts_with")) |str_starts_with_ident| { + try low_level_map.put(str_starts_with_ident, .str_starts_with); + } + if (env.common.findIdent("Builtin.Str.ends_with")) |str_ends_with_ident| { + try low_level_map.put(str_ends_with_ident, .str_ends_with); + } + if (env.common.findIdent("Builtin.Str.repeat")) |str_repeat_ident| { + try low_level_map.put(str_repeat_ident, .str_repeat); + } + if (env.common.findIdent("Builtin.Str.with_prefix")) |str_with_prefix_ident| { + try low_level_map.put(str_with_prefix_ident, .str_with_prefix); + } + if (env.common.findIdent("Builtin.Str.drop_prefix")) |str_drop_prefix_ident| { + try low_level_map.put(str_drop_prefix_ident, .str_drop_prefix); + } + if (env.common.findIdent("Builtin.Str.drop_suffix")) |str_drop_suffix_ident| { + try low_level_map.put(str_drop_suffix_ident, .str_drop_suffix); + } + if (env.common.findIdent("Builtin.Str.count_utf8_bytes")) |str_count_utf8_bytes_ident| { + try low_level_map.put(str_count_utf8_bytes_ident, .str_count_utf8_bytes); + } + if (env.common.findIdent("Builtin.Str.with_capacity")) |str_with_capacity_ident| { + try low_level_map.put(str_with_capacity_ident, .str_with_capacity); + } + if (env.common.findIdent("Builtin.Str.reserve")) |str_reserve_ident| { + try low_level_map.put(str_reserve_ident, .str_reserve); + } + if (env.common.findIdent("Builtin.Str.release_excess_capacity")) |str_release_excess_capacity_ident| { + try low_level_map.put(str_release_excess_capacity_ident, .str_release_excess_capacity); + } + if (env.common.findIdent("Builtin.Str.to_utf8")) |str_to_utf8_ident| { + try low_level_map.put(str_to_utf8_ident, .str_to_utf8); + } + if (env.common.findIdent("Builtin.Str.from_utf8_lossy")) |str_from_utf8_lossy_ident| { + try low_level_map.put(str_from_utf8_lossy_ident, .str_from_utf8_lossy); + } + if (env.common.findIdent("Builtin.Str.from_utf8")) |str_from_utf8_ident| { + try low_level_map.put(str_from_utf8_ident, .str_from_utf8); + } + if (env.common.findIdent("Builtin.Str.split_on")) |str_split_on_ident| { + try low_level_map.put(str_split_on_ident, .str_split_on); + } + if (env.common.findIdent("Builtin.Str.join_with")) |str_join_with_ident| { + try low_level_map.put(str_join_with_ident, .str_join_with); + } + if (env.common.findIdent("Builtin.Str.inspect")) |str_inspekt_ident| { + try low_level_map.put(str_inspekt_ident, .str_inspekt); + } + if (env.common.findIdent("Builtin.List.len")) |list_len_ident| { + try low_level_map.put(list_len_ident, .list_len); + } + if (env.common.findIdent("Builtin.List.is_empty")) |list_is_empty_ident| { + try low_level_map.put(list_is_empty_ident, .list_is_empty); + } + if (env.common.findIdent("Builtin.List.concat")) |list_concat_ident| { + try low_level_map.put(list_concat_ident, .list_concat); + } + if (env.common.findIdent("Builtin.List.append")) |list_append_ident| { + try low_level_map.put(list_append_ident, .list_append); + } + if (env.common.findIdent("Builtin.List.with_capacity")) |list_with_capacity_ident| { + try low_level_map.put(list_with_capacity_ident, .list_with_capacity); + } + if (env.common.findIdent("Builtin.List.sort_with")) |list_sort_with_ident| { + try low_level_map.put(list_sort_with_ident, .list_sort_with); + } + if (env.common.findIdent("list_get_unsafe")) |list_get_unsafe_ident| { + try low_level_map.put(list_get_unsafe_ident, .list_get_unsafe); + } + if (env.common.findIdent("list_append_unsafe")) |list_append_unsafe_ident| { + try low_level_map.put(list_append_unsafe_ident, .list_append_unsafe); + } + if (env.common.findIdent("Builtin.List.drop_at")) |list_drop_at_ident| { + try low_level_map.put(list_drop_at_ident, .list_drop_at); + } + if (env.common.findIdent("Builtin.List.sublist")) |list_sublist_ident| { + try low_level_map.put(list_sublist_ident, .list_sublist); + } + if (env.common.findIdent("Builtin.Bool.is_eq")) |bool_is_eq_ident| { + try low_level_map.put(bool_is_eq_ident, .bool_is_eq); + } + + // Numeric type checking operations (all numeric types) + const numeric_types = [_][]const u8{ "U8", "I8", "U16", "I16", "U32", "I32", "U64", "I64", "U128", "I128", "Dec", "F32", "F64" }; + for (numeric_types) |num_type| { + var buf: [256]u8 = undefined; + + // is_zero (all types) + const is_zero = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.is_zero", .{num_type}); + if (env.common.findIdent(is_zero)) |ident| { + try low_level_map.put(ident, .num_is_zero); + } + } + + // Numeric sign checking operations (signed types only) + const signed_types = [_][]const u8{ "I8", "I16", "I32", "I64", "I128", "Dec", "F32", "F64" }; + for (signed_types) |num_type| { + var buf: [256]u8 = undefined; + + // is_negative + const is_negative = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.is_negative", .{num_type}); + if (env.common.findIdent(is_negative)) |ident| { + try low_level_map.put(ident, .num_is_negative); + } + + // is_positive + const is_positive = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.is_positive", .{num_type}); + if (env.common.findIdent(is_positive)) |ident| { + try low_level_map.put(ident, .num_is_positive); + } + } + + // Numeric equality operations (integer types + Dec only, NOT F32/F64) + const eq_types = [_][]const u8{ "U8", "I8", "U16", "I16", "U32", "I32", "U64", "I64", "U128", "I128", "Dec" }; + for (eq_types) |num_type| { + var buf: [256]u8 = undefined; + + // is_eq + const is_eq = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.is_eq", .{num_type}); + if (env.common.findIdent(is_eq)) |ident| { + try low_level_map.put(ident, .num_is_eq); + } + } + + // Numeric to_str operations (all numeric types) + // Note: Types like Dec are nested under Num in Builtin.roc, so the canonical identifier is + // "Builtin.Num.Dec.to_str". But Dec is auto-imported as "Dec", so user code + // calling Dec.to_str looks up "Builtin.Dec.to_str". We need the canonical name here. + for (numeric_types) |num_type| { + var buf: [256]u8 = undefined; + const to_str_name = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.to_str", .{num_type}); + if (env.common.findIdent(to_str_name)) |ident| { + const low_level_op: CIR.Expr.LowLevel = if (std.mem.eql(u8, num_type, "U8")) + .u8_to_str + else if (std.mem.eql(u8, num_type, "I8")) + .i8_to_str + else if (std.mem.eql(u8, num_type, "U16")) + .u16_to_str + else if (std.mem.eql(u8, num_type, "I16")) + .i16_to_str + else if (std.mem.eql(u8, num_type, "U32")) + .u32_to_str + else if (std.mem.eql(u8, num_type, "I32")) + .i32_to_str + else if (std.mem.eql(u8, num_type, "U64")) + .u64_to_str + else if (std.mem.eql(u8, num_type, "I64")) + .i64_to_str + else if (std.mem.eql(u8, num_type, "U128")) + .u128_to_str + else if (std.mem.eql(u8, num_type, "I128")) + .i128_to_str + else if (std.mem.eql(u8, num_type, "Dec")) + .dec_to_str + else if (std.mem.eql(u8, num_type, "F32")) + .f32_to_str + else if (std.mem.eql(u8, num_type, "F64")) + .f64_to_str + else + continue; + try low_level_map.put(ident, low_level_op); + } + } + + // Numeric comparison operations (all numeric types) + for (numeric_types) |num_type| { + var buf: [256]u8 = undefined; + + // is_gt + const is_gt = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.is_gt", .{num_type}); + if (env.common.findIdent(is_gt)) |ident| { + try low_level_map.put(ident, .num_is_gt); + } + + // is_gte + const is_gte = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.is_gte", .{num_type}); + if (env.common.findIdent(is_gte)) |ident| { + try low_level_map.put(ident, .num_is_gte); + } + + // is_lt + const is_lt = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.is_lt", .{num_type}); + if (env.common.findIdent(is_lt)) |ident| { + try low_level_map.put(ident, .num_is_lt); + } + + // is_lte + const is_lte = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.is_lte", .{num_type}); + if (env.common.findIdent(is_lte)) |ident| { + try low_level_map.put(ident, .num_is_lte); + } + } + + // Numeric parsing operations (all numeric types have from_int_digits) + for (numeric_types) |num_type| { + var buf: [256]u8 = undefined; + + // from_int_digits + const from_int_digits = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.from_int_digits", .{num_type}); + if (env.common.findIdent(from_int_digits)) |ident| { + try low_level_map.put(ident, .num_from_int_digits); + } + } + + // from_dec_digits (Dec, F32, F64 only) + const dec_types = [_][]const u8{ "Dec", "F32", "F64" }; + for (dec_types) |num_type| { + var buf: [256]u8 = undefined; + + // from_dec_digits + const from_dec_digits = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.from_dec_digits", .{num_type}); + if (env.common.findIdent(from_dec_digits)) |ident| { + try low_level_map.put(ident, .num_from_dec_digits); + } + } + + // from_numeral (all numeric types) + for (numeric_types) |num_type| { + var buf: [256]u8 = undefined; + + const from_numeral = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.from_numeral", .{num_type}); + if (env.common.findIdent(from_numeral)) |ident| { + try low_level_map.put(ident, .num_from_numeral); + } + } + + // from_str (all numeric types) + for (numeric_types) |num_type| { + var buf: [256]u8 = undefined; + + const from_str = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.from_str", .{num_type}); + if (env.common.findIdent(from_str)) |ident| { + try low_level_map.put(ident, .num_from_str); + } + } + + // Numeric arithmetic operations (all numeric types have plus, minus, times, div_by, rem_by) + for (numeric_types) |num_type| { + var buf: [256]u8 = undefined; + + // plus + const plus = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.plus", .{num_type}); + if (env.common.findIdent(plus)) |ident| { + try low_level_map.put(ident, .num_plus); + } + + // minus + const minus = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.minus", .{num_type}); + if (env.common.findIdent(minus)) |ident| { + try low_level_map.put(ident, .num_minus); + } + + // times + const times = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.times", .{num_type}); + if (env.common.findIdent(times)) |ident| { + try low_level_map.put(ident, .num_times); + } + + // div_by + const div_by = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.div_by", .{num_type}); + if (env.common.findIdent(div_by)) |ident| { + try low_level_map.put(ident, .num_div_by); + } + + // div_trunc_by + const div_trunc_by = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.div_trunc_by", .{num_type}); + if (env.common.findIdent(div_trunc_by)) |ident| { + try low_level_map.put(ident, .num_div_trunc_by); + } + + // rem_by + const rem_by = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.rem_by", .{num_type}); + if (env.common.findIdent(rem_by)) |ident| { + try low_level_map.put(ident, .num_rem_by); + } + } + + // Numeric modulo operation (integer types only) + const integer_types = [_][]const u8{ "U8", "I8", "U16", "I16", "U32", "I32", "U64", "I64", "U128", "I128" }; + for (integer_types) |num_type| { + var buf: [256]u8 = undefined; + + // mod_by + const mod_by = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.mod_by", .{num_type}); + if (env.common.findIdent(mod_by)) |ident| { + try low_level_map.put(ident, .num_mod_by); + } + } + + // Numeric negate operation (signed types only) + for (signed_types) |num_type| { + var buf: [256]u8 = undefined; + + // negate + const negate = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.negate", .{num_type}); + if (env.common.findIdent(negate)) |ident| { + try low_level_map.put(ident, .num_negate); + } + + // abs + const abs = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.abs", .{num_type}); + if (env.common.findIdent(abs)) |ident| { + try low_level_map.put(ident, .num_abs); + } + } + + // Numeric abs_diff operation (all numeric types) + for (numeric_types) |num_type| { + var buf: [256]u8 = undefined; + + const abs_diff = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.abs_diff", .{num_type}); + if (env.common.findIdent(abs_diff)) |ident| { + try low_level_map.put(ident, .num_abs_diff); + } + } + + // Bitwise shift operations (integer types only); + for (integer_types) |num_type| { + var buf: [256]u8 = undefined; + + // shift_left_by + const shift_left_by = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.shift_left_by", .{num_type}); + if (env.common.findIdent(shift_left_by)) |ident| { + try low_level_map.put(ident, .num_shift_left_by); + } + + // shift_right_by + const shift_right_by = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.shift_right_by", .{num_type}); + if (env.common.findIdent(shift_right_by)) |ident| { + try low_level_map.put(ident, .num_shift_right_by); + } + + // shift_right_zf_by + const shift_right_zf_by = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.shift_right_zf_by", .{num_type}); + if (env.common.findIdent(shift_right_zf_by)) |ident| { + try low_level_map.put(ident, .num_shift_right_zf_by); + } + } + + // U8 conversion operations + if (env.common.findIdent("Builtin.Num.U8.to_i8_wrap")) |ident| { + try low_level_map.put(ident, .u8_to_i8_wrap); + } + if (env.common.findIdent("Builtin.Num.U8.to_i8_try")) |ident| { + try low_level_map.put(ident, .u8_to_i8_try); + } + if (env.common.findIdent("Builtin.Num.U8.to_i16")) |ident| { + try low_level_map.put(ident, .u8_to_i16); + } + if (env.common.findIdent("Builtin.Num.U8.to_i32")) |ident| { + try low_level_map.put(ident, .u8_to_i32); + } + if (env.common.findIdent("Builtin.Num.U8.to_i64")) |ident| { + try low_level_map.put(ident, .u8_to_i64); + } + if (env.common.findIdent("Builtin.Num.U8.to_i128")) |ident| { + try low_level_map.put(ident, .u8_to_i128); + } + if (env.common.findIdent("Builtin.Num.U8.to_u16")) |ident| { + try low_level_map.put(ident, .u8_to_u16); + } + if (env.common.findIdent("Builtin.Num.U8.to_u32")) |ident| { + try low_level_map.put(ident, .u8_to_u32); + } + if (env.common.findIdent("Builtin.Num.U8.to_u64")) |ident| { + try low_level_map.put(ident, .u8_to_u64); + } + if (env.common.findIdent("Builtin.Num.U8.to_u128")) |ident| { + try low_level_map.put(ident, .u8_to_u128); + } + if (env.common.findIdent("Builtin.Num.U8.to_f32")) |ident| { + try low_level_map.put(ident, .u8_to_f32); + } + if (env.common.findIdent("Builtin.Num.U8.to_f64")) |ident| { + try low_level_map.put(ident, .u8_to_f64); + } + if (env.common.findIdent("Builtin.Num.U8.to_dec")) |ident| { + try low_level_map.put(ident, .u8_to_dec); + } + + // I8 conversion operations + if (env.common.findIdent("Builtin.Num.I8.to_i16")) |ident| { + try low_level_map.put(ident, .i8_to_i16); + } + if (env.common.findIdent("Builtin.Num.I8.to_i32")) |ident| { + try low_level_map.put(ident, .i8_to_i32); + } + if (env.common.findIdent("Builtin.Num.I8.to_i64")) |ident| { + try low_level_map.put(ident, .i8_to_i64); + } + if (env.common.findIdent("Builtin.Num.I8.to_i128")) |ident| { + try low_level_map.put(ident, .i8_to_i128); + } + if (env.common.findIdent("Builtin.Num.I8.to_u8_wrap")) |ident| { + try low_level_map.put(ident, .i8_to_u8_wrap); + } + if (env.common.findIdent("Builtin.Num.I8.to_u8_try")) |ident| { + try low_level_map.put(ident, .i8_to_u8_try); + } + if (env.common.findIdent("Builtin.Num.I8.to_u16_wrap")) |ident| { + try low_level_map.put(ident, .i8_to_u16_wrap); + } + if (env.common.findIdent("Builtin.Num.I8.to_u16_try")) |ident| { + try low_level_map.put(ident, .i8_to_u16_try); + } + if (env.common.findIdent("Builtin.Num.I8.to_u32_wrap")) |ident| { + try low_level_map.put(ident, .i8_to_u32_wrap); + } + if (env.common.findIdent("Builtin.Num.I8.to_u32_try")) |ident| { + try low_level_map.put(ident, .i8_to_u32_try); + } + if (env.common.findIdent("Builtin.Num.I8.to_u64_wrap")) |ident| { + try low_level_map.put(ident, .i8_to_u64_wrap); + } + if (env.common.findIdent("Builtin.Num.I8.to_u64_try")) |ident| { + try low_level_map.put(ident, .i8_to_u64_try); + } + if (env.common.findIdent("Builtin.Num.I8.to_u128_wrap")) |ident| { + try low_level_map.put(ident, .i8_to_u128_wrap); + } + if (env.common.findIdent("Builtin.Num.I8.to_u128_try")) |ident| { + try low_level_map.put(ident, .i8_to_u128_try); + } + if (env.common.findIdent("Builtin.Num.I8.to_f32")) |ident| { + try low_level_map.put(ident, .i8_to_f32); + } + if (env.common.findIdent("Builtin.Num.I8.to_f64")) |ident| { + try low_level_map.put(ident, .i8_to_f64); + } + if (env.common.findIdent("Builtin.Num.I8.to_dec")) |ident| { + try low_level_map.put(ident, .i8_to_dec); + } + + // U16 conversion operations + if (env.common.findIdent("Builtin.Num.U16.to_i8_wrap")) |ident| { + try low_level_map.put(ident, .u16_to_i8_wrap); + } + if (env.common.findIdent("Builtin.Num.U16.to_i8_try")) |ident| { + try low_level_map.put(ident, .u16_to_i8_try); + } + if (env.common.findIdent("Builtin.Num.U16.to_i16_wrap")) |ident| { + try low_level_map.put(ident, .u16_to_i16_wrap); + } + if (env.common.findIdent("Builtin.Num.U16.to_i16_try")) |ident| { + try low_level_map.put(ident, .u16_to_i16_try); + } + if (env.common.findIdent("Builtin.Num.U16.to_i32")) |ident| { + try low_level_map.put(ident, .u16_to_i32); + } + if (env.common.findIdent("Builtin.Num.U16.to_i64")) |ident| { + try low_level_map.put(ident, .u16_to_i64); + } + if (env.common.findIdent("Builtin.Num.U16.to_i128")) |ident| { + try low_level_map.put(ident, .u16_to_i128); + } + if (env.common.findIdent("Builtin.Num.U16.to_u8_wrap")) |ident| { + try low_level_map.put(ident, .u16_to_u8_wrap); + } + if (env.common.findIdent("Builtin.Num.U16.to_u8_try")) |ident| { + try low_level_map.put(ident, .u16_to_u8_try); + } + if (env.common.findIdent("Builtin.Num.U16.to_u32")) |ident| { + try low_level_map.put(ident, .u16_to_u32); + } + if (env.common.findIdent("Builtin.Num.U16.to_u64")) |ident| { + try low_level_map.put(ident, .u16_to_u64); + } + if (env.common.findIdent("Builtin.Num.U16.to_u128")) |ident| { + try low_level_map.put(ident, .u16_to_u128); + } + if (env.common.findIdent("Builtin.Num.U16.to_f32")) |ident| { + try low_level_map.put(ident, .u16_to_f32); + } + if (env.common.findIdent("Builtin.Num.U16.to_f64")) |ident| { + try low_level_map.put(ident, .u16_to_f64); + } + if (env.common.findIdent("Builtin.Num.U16.to_dec")) |ident| { + try low_level_map.put(ident, .u16_to_dec); + } + + // I16 conversion operations + if (env.common.findIdent("Builtin.Num.I16.to_i8_wrap")) |ident| { + try low_level_map.put(ident, .i16_to_i8_wrap); + } + if (env.common.findIdent("Builtin.Num.I16.to_i8_try")) |ident| { + try low_level_map.put(ident, .i16_to_i8_try); + } + if (env.common.findIdent("Builtin.Num.I16.to_i32")) |ident| { + try low_level_map.put(ident, .i16_to_i32); + } + if (env.common.findIdent("Builtin.Num.I16.to_i64")) |ident| { + try low_level_map.put(ident, .i16_to_i64); + } + if (env.common.findIdent("Builtin.Num.I16.to_i128")) |ident| { + try low_level_map.put(ident, .i16_to_i128); + } + if (env.common.findIdent("Builtin.Num.I16.to_u8_wrap")) |ident| { + try low_level_map.put(ident, .i16_to_u8_wrap); + } + if (env.common.findIdent("Builtin.Num.I16.to_u8_try")) |ident| { + try low_level_map.put(ident, .i16_to_u8_try); + } + if (env.common.findIdent("Builtin.Num.I16.to_u16_wrap")) |ident| { + try low_level_map.put(ident, .i16_to_u16_wrap); + } + if (env.common.findIdent("Builtin.Num.I16.to_u16_try")) |ident| { + try low_level_map.put(ident, .i16_to_u16_try); + } + if (env.common.findIdent("Builtin.Num.I16.to_u32_wrap")) |ident| { + try low_level_map.put(ident, .i16_to_u32_wrap); + } + if (env.common.findIdent("Builtin.Num.I16.to_u32_try")) |ident| { + try low_level_map.put(ident, .i16_to_u32_try); + } + if (env.common.findIdent("Builtin.Num.I16.to_u64_wrap")) |ident| { + try low_level_map.put(ident, .i16_to_u64_wrap); + } + if (env.common.findIdent("Builtin.Num.I16.to_u64_try")) |ident| { + try low_level_map.put(ident, .i16_to_u64_try); + } + if (env.common.findIdent("Builtin.Num.I16.to_u128_wrap")) |ident| { + try low_level_map.put(ident, .i16_to_u128_wrap); + } + if (env.common.findIdent("Builtin.Num.I16.to_u128_try")) |ident| { + try low_level_map.put(ident, .i16_to_u128_try); + } + if (env.common.findIdent("Builtin.Num.I16.to_f32")) |ident| { + try low_level_map.put(ident, .i16_to_f32); + } + if (env.common.findIdent("Builtin.Num.I16.to_f64")) |ident| { + try low_level_map.put(ident, .i16_to_f64); + } + if (env.common.findIdent("Builtin.Num.I16.to_dec")) |ident| { + try low_level_map.put(ident, .i16_to_dec); + } + + // U32 conversion operations + if (env.common.findIdent("Builtin.Num.U32.to_i8_wrap")) |ident| { + try low_level_map.put(ident, .u32_to_i8_wrap); + } + if (env.common.findIdent("Builtin.Num.U32.to_i8_try")) |ident| { + try low_level_map.put(ident, .u32_to_i8_try); + } + if (env.common.findIdent("Builtin.Num.U32.to_i16_wrap")) |ident| { + try low_level_map.put(ident, .u32_to_i16_wrap); + } + if (env.common.findIdent("Builtin.Num.U32.to_i16_try")) |ident| { + try low_level_map.put(ident, .u32_to_i16_try); + } + if (env.common.findIdent("Builtin.Num.U32.to_i32_wrap")) |ident| { + try low_level_map.put(ident, .u32_to_i32_wrap); + } + if (env.common.findIdent("Builtin.Num.U32.to_i32_try")) |ident| { + try low_level_map.put(ident, .u32_to_i32_try); + } + if (env.common.findIdent("Builtin.Num.U32.to_i64")) |ident| { + try low_level_map.put(ident, .u32_to_i64); + } + if (env.common.findIdent("Builtin.Num.U32.to_i128")) |ident| { + try low_level_map.put(ident, .u32_to_i128); + } + if (env.common.findIdent("Builtin.Num.U32.to_u8_wrap")) |ident| { + try low_level_map.put(ident, .u32_to_u8_wrap); + } + if (env.common.findIdent("Builtin.Num.U32.to_u8_try")) |ident| { + try low_level_map.put(ident, .u32_to_u8_try); + } + if (env.common.findIdent("Builtin.Num.U32.to_u16_wrap")) |ident| { + try low_level_map.put(ident, .u32_to_u16_wrap); + } + if (env.common.findIdent("Builtin.Num.U32.to_u16_try")) |ident| { + try low_level_map.put(ident, .u32_to_u16_try); + } + if (env.common.findIdent("Builtin.Num.U32.to_u64")) |ident| { + try low_level_map.put(ident, .u32_to_u64); + } + if (env.common.findIdent("Builtin.Num.U32.to_u128")) |ident| { + try low_level_map.put(ident, .u32_to_u128); + } + if (env.common.findIdent("Builtin.Num.U32.to_f32")) |ident| { + try low_level_map.put(ident, .u32_to_f32); + } + if (env.common.findIdent("Builtin.Num.U32.to_f64")) |ident| { + try low_level_map.put(ident, .u32_to_f64); + } + if (env.common.findIdent("Builtin.Num.U32.to_dec")) |ident| { + try low_level_map.put(ident, .u32_to_dec); + } + + // I32 conversion operations + if (env.common.findIdent("Builtin.Num.I32.to_i8_wrap")) |ident| { + try low_level_map.put(ident, .i32_to_i8_wrap); + } + if (env.common.findIdent("Builtin.Num.I32.to_i8_try")) |ident| { + try low_level_map.put(ident, .i32_to_i8_try); + } + if (env.common.findIdent("Builtin.Num.I32.to_i16_wrap")) |ident| { + try low_level_map.put(ident, .i32_to_i16_wrap); + } + if (env.common.findIdent("Builtin.Num.I32.to_i16_try")) |ident| { + try low_level_map.put(ident, .i32_to_i16_try); + } + if (env.common.findIdent("Builtin.Num.I32.to_i64")) |ident| { + try low_level_map.put(ident, .i32_to_i64); + } + if (env.common.findIdent("Builtin.Num.I32.to_i128")) |ident| { + try low_level_map.put(ident, .i32_to_i128); + } + if (env.common.findIdent("Builtin.Num.I32.to_u8_wrap")) |ident| { + try low_level_map.put(ident, .i32_to_u8_wrap); + } + if (env.common.findIdent("Builtin.Num.I32.to_u8_try")) |ident| { + try low_level_map.put(ident, .i32_to_u8_try); + } + if (env.common.findIdent("Builtin.Num.I32.to_u16_wrap")) |ident| { + try low_level_map.put(ident, .i32_to_u16_wrap); + } + if (env.common.findIdent("Builtin.Num.I32.to_u16_try")) |ident| { + try low_level_map.put(ident, .i32_to_u16_try); + } + if (env.common.findIdent("Builtin.Num.I32.to_u32_wrap")) |ident| { + try low_level_map.put(ident, .i32_to_u32_wrap); + } + if (env.common.findIdent("Builtin.Num.I32.to_u32_try")) |ident| { + try low_level_map.put(ident, .i32_to_u32_try); + } + if (env.common.findIdent("Builtin.Num.I32.to_u64_wrap")) |ident| { + try low_level_map.put(ident, .i32_to_u64_wrap); + } + if (env.common.findIdent("Builtin.Num.I32.to_u64_try")) |ident| { + try low_level_map.put(ident, .i32_to_u64_try); + } + if (env.common.findIdent("Builtin.Num.I32.to_u128_wrap")) |ident| { + try low_level_map.put(ident, .i32_to_u128_wrap); + } + if (env.common.findIdent("Builtin.Num.I32.to_u128_try")) |ident| { + try low_level_map.put(ident, .i32_to_u128_try); + } + if (env.common.findIdent("Builtin.Num.I32.to_f32")) |ident| { + try low_level_map.put(ident, .i32_to_f32); + } + if (env.common.findIdent("Builtin.Num.I32.to_f64")) |ident| { + try low_level_map.put(ident, .i32_to_f64); + } + if (env.common.findIdent("Builtin.Num.I32.to_dec")) |ident| { + try low_level_map.put(ident, .i32_to_dec); + } + + // U64 conversion operations + if (env.common.findIdent("Builtin.Num.U64.to_i8_wrap")) |ident| { + try low_level_map.put(ident, .u64_to_i8_wrap); + } + if (env.common.findIdent("Builtin.Num.U64.to_i8_try")) |ident| { + try low_level_map.put(ident, .u64_to_i8_try); + } + if (env.common.findIdent("Builtin.Num.U64.to_i16_wrap")) |ident| { + try low_level_map.put(ident, .u64_to_i16_wrap); + } + if (env.common.findIdent("Builtin.Num.U64.to_i16_try")) |ident| { + try low_level_map.put(ident, .u64_to_i16_try); + } + if (env.common.findIdent("Builtin.Num.U64.to_i32_wrap")) |ident| { + try low_level_map.put(ident, .u64_to_i32_wrap); + } + if (env.common.findIdent("Builtin.Num.U64.to_i32_try")) |ident| { + try low_level_map.put(ident, .u64_to_i32_try); + } + if (env.common.findIdent("Builtin.Num.U64.to_i64_wrap")) |ident| { + try low_level_map.put(ident, .u64_to_i64_wrap); + } + if (env.common.findIdent("Builtin.Num.U64.to_i64_try")) |ident| { + try low_level_map.put(ident, .u64_to_i64_try); + } + if (env.common.findIdent("Builtin.Num.U64.to_i128")) |ident| { + try low_level_map.put(ident, .u64_to_i128); + } + if (env.common.findIdent("Builtin.Num.U64.to_u8_wrap")) |ident| { + try low_level_map.put(ident, .u64_to_u8_wrap); + } + if (env.common.findIdent("Builtin.Num.U64.to_u8_try")) |ident| { + try low_level_map.put(ident, .u64_to_u8_try); + } + if (env.common.findIdent("Builtin.Num.U64.to_u16_wrap")) |ident| { + try low_level_map.put(ident, .u64_to_u16_wrap); + } + if (env.common.findIdent("Builtin.Num.U64.to_u16_try")) |ident| { + try low_level_map.put(ident, .u64_to_u16_try); + } + if (env.common.findIdent("Builtin.Num.U64.to_u32_wrap")) |ident| { + try low_level_map.put(ident, .u64_to_u32_wrap); + } + if (env.common.findIdent("Builtin.Num.U64.to_u32_try")) |ident| { + try low_level_map.put(ident, .u64_to_u32_try); + } + if (env.common.findIdent("Builtin.Num.U64.to_u128")) |ident| { + try low_level_map.put(ident, .u64_to_u128); + } + if (env.common.findIdent("Builtin.Num.U64.to_f32")) |ident| { + try low_level_map.put(ident, .u64_to_f32); + } + if (env.common.findIdent("Builtin.Num.U64.to_f64")) |ident| { + try low_level_map.put(ident, .u64_to_f64); + } + if (env.common.findIdent("Builtin.Num.U64.to_dec")) |ident| { + try low_level_map.put(ident, .u64_to_dec); + } + + // I64 conversion operations + if (env.common.findIdent("Builtin.Num.I64.to_i8_wrap")) |ident| { + try low_level_map.put(ident, .i64_to_i8_wrap); + } + if (env.common.findIdent("Builtin.Num.I64.to_i8_try")) |ident| { + try low_level_map.put(ident, .i64_to_i8_try); + } + if (env.common.findIdent("Builtin.Num.I64.to_i16_wrap")) |ident| { + try low_level_map.put(ident, .i64_to_i16_wrap); + } + if (env.common.findIdent("Builtin.Num.I64.to_i16_try")) |ident| { + try low_level_map.put(ident, .i64_to_i16_try); + } + if (env.common.findIdent("Builtin.Num.I64.to_i32_wrap")) |ident| { + try low_level_map.put(ident, .i64_to_i32_wrap); + } + if (env.common.findIdent("Builtin.Num.I64.to_i32_try")) |ident| { + try low_level_map.put(ident, .i64_to_i32_try); + } + if (env.common.findIdent("Builtin.Num.I64.to_i128")) |ident| { + try low_level_map.put(ident, .i64_to_i128); + } + if (env.common.findIdent("Builtin.Num.I64.to_u8_wrap")) |ident| { + try low_level_map.put(ident, .i64_to_u8_wrap); + } + if (env.common.findIdent("Builtin.Num.I64.to_u8_try")) |ident| { + try low_level_map.put(ident, .i64_to_u8_try); + } + if (env.common.findIdent("Builtin.Num.I64.to_u16_wrap")) |ident| { + try low_level_map.put(ident, .i64_to_u16_wrap); + } + if (env.common.findIdent("Builtin.Num.I64.to_u16_try")) |ident| { + try low_level_map.put(ident, .i64_to_u16_try); + } + if (env.common.findIdent("Builtin.Num.I64.to_u32_wrap")) |ident| { + try low_level_map.put(ident, .i64_to_u32_wrap); + } + if (env.common.findIdent("Builtin.Num.I64.to_u32_try")) |ident| { + try low_level_map.put(ident, .i64_to_u32_try); + } + if (env.common.findIdent("Builtin.Num.I64.to_u64_wrap")) |ident| { + try low_level_map.put(ident, .i64_to_u64_wrap); + } + if (env.common.findIdent("Builtin.Num.I64.to_u64_try")) |ident| { + try low_level_map.put(ident, .i64_to_u64_try); + } + if (env.common.findIdent("Builtin.Num.I64.to_u128_wrap")) |ident| { + try low_level_map.put(ident, .i64_to_u128_wrap); + } + if (env.common.findIdent("Builtin.Num.I64.to_u128_try")) |ident| { + try low_level_map.put(ident, .i64_to_u128_try); + } + if (env.common.findIdent("Builtin.Num.I64.to_f32")) |ident| { + try low_level_map.put(ident, .i64_to_f32); + } + if (env.common.findIdent("Builtin.Num.I64.to_f64")) |ident| { + try low_level_map.put(ident, .i64_to_f64); + } + if (env.common.findIdent("Builtin.Num.I64.to_dec")) |ident| { + try low_level_map.put(ident, .i64_to_dec); + } + + // U128 conversion operations + if (env.common.findIdent("Builtin.Num.U128.to_i8_wrap")) |ident| { + try low_level_map.put(ident, .u128_to_i8_wrap); + } + if (env.common.findIdent("Builtin.Num.U128.to_i8_try")) |ident| { + try low_level_map.put(ident, .u128_to_i8_try); + } + if (env.common.findIdent("Builtin.Num.U128.to_i16_wrap")) |ident| { + try low_level_map.put(ident, .u128_to_i16_wrap); + } + if (env.common.findIdent("Builtin.Num.U128.to_i16_try")) |ident| { + try low_level_map.put(ident, .u128_to_i16_try); + } + if (env.common.findIdent("Builtin.Num.U128.to_i32_wrap")) |ident| { + try low_level_map.put(ident, .u128_to_i32_wrap); + } + if (env.common.findIdent("Builtin.Num.U128.to_i32_try")) |ident| { + try low_level_map.put(ident, .u128_to_i32_try); + } + if (env.common.findIdent("Builtin.Num.U128.to_i64_wrap")) |ident| { + try low_level_map.put(ident, .u128_to_i64_wrap); + } + if (env.common.findIdent("Builtin.Num.U128.to_i64_try")) |ident| { + try low_level_map.put(ident, .u128_to_i64_try); + } + if (env.common.findIdent("Builtin.Num.U128.to_i128_wrap")) |ident| { + try low_level_map.put(ident, .u128_to_i128_wrap); + } + if (env.common.findIdent("Builtin.Num.U128.to_i128_try")) |ident| { + try low_level_map.put(ident, .u128_to_i128_try); + } + if (env.common.findIdent("Builtin.Num.U128.to_u8_wrap")) |ident| { + try low_level_map.put(ident, .u128_to_u8_wrap); + } + if (env.common.findIdent("Builtin.Num.U128.to_u8_try")) |ident| { + try low_level_map.put(ident, .u128_to_u8_try); + } + if (env.common.findIdent("Builtin.Num.U128.to_u16_wrap")) |ident| { + try low_level_map.put(ident, .u128_to_u16_wrap); + } + if (env.common.findIdent("Builtin.Num.U128.to_u16_try")) |ident| { + try low_level_map.put(ident, .u128_to_u16_try); + } + if (env.common.findIdent("Builtin.Num.U128.to_u32_wrap")) |ident| { + try low_level_map.put(ident, .u128_to_u32_wrap); + } + if (env.common.findIdent("Builtin.Num.U128.to_u32_try")) |ident| { + try low_level_map.put(ident, .u128_to_u32_try); + } + if (env.common.findIdent("Builtin.Num.U128.to_u64_wrap")) |ident| { + try low_level_map.put(ident, .u128_to_u64_wrap); + } + if (env.common.findIdent("Builtin.Num.U128.to_u64_try")) |ident| { + try low_level_map.put(ident, .u128_to_u64_try); + } + if (env.common.findIdent("Builtin.Num.U128.to_f32")) |ident| { + try low_level_map.put(ident, .u128_to_f32); + } + if (env.common.findIdent("Builtin.Num.U128.to_f64")) |ident| { + try low_level_map.put(ident, .u128_to_f64); + } + if (env.common.findIdent("u128_to_dec_try_unsafe")) |ident| { + try low_level_map.put(ident, .u128_to_dec_try_unsafe); + } + + // I128 conversion operations + if (env.common.findIdent("Builtin.Num.I128.to_i8_wrap")) |ident| { + try low_level_map.put(ident, .i128_to_i8_wrap); + } + if (env.common.findIdent("Builtin.Num.I128.to_i8_try")) |ident| { + try low_level_map.put(ident, .i128_to_i8_try); + } + if (env.common.findIdent("Builtin.Num.I128.to_i16_wrap")) |ident| { + try low_level_map.put(ident, .i128_to_i16_wrap); + } + if (env.common.findIdent("Builtin.Num.I128.to_i16_try")) |ident| { + try low_level_map.put(ident, .i128_to_i16_try); + } + if (env.common.findIdent("Builtin.Num.I128.to_i32_wrap")) |ident| { + try low_level_map.put(ident, .i128_to_i32_wrap); + } + if (env.common.findIdent("Builtin.Num.I128.to_i32_try")) |ident| { + try low_level_map.put(ident, .i128_to_i32_try); + } + if (env.common.findIdent("Builtin.Num.I128.to_i64_wrap")) |ident| { + try low_level_map.put(ident, .i128_to_i64_wrap); + } + if (env.common.findIdent("Builtin.Num.I128.to_i64_try")) |ident| { + try low_level_map.put(ident, .i128_to_i64_try); + } + if (env.common.findIdent("Builtin.Num.I128.to_u8_wrap")) |ident| { + try low_level_map.put(ident, .i128_to_u8_wrap); + } + if (env.common.findIdent("Builtin.Num.I128.to_u8_try")) |ident| { + try low_level_map.put(ident, .i128_to_u8_try); + } + if (env.common.findIdent("Builtin.Num.I128.to_u16_wrap")) |ident| { + try low_level_map.put(ident, .i128_to_u16_wrap); + } + if (env.common.findIdent("Builtin.Num.I128.to_u16_try")) |ident| { + try low_level_map.put(ident, .i128_to_u16_try); + } + if (env.common.findIdent("Builtin.Num.I128.to_u32_wrap")) |ident| { + try low_level_map.put(ident, .i128_to_u32_wrap); + } + if (env.common.findIdent("Builtin.Num.I128.to_u32_try")) |ident| { + try low_level_map.put(ident, .i128_to_u32_try); + } + if (env.common.findIdent("Builtin.Num.I128.to_u64_wrap")) |ident| { + try low_level_map.put(ident, .i128_to_u64_wrap); + } + if (env.common.findIdent("Builtin.Num.I128.to_u64_try")) |ident| { + try low_level_map.put(ident, .i128_to_u64_try); + } + if (env.common.findIdent("Builtin.Num.I128.to_u128_wrap")) |ident| { + try low_level_map.put(ident, .i128_to_u128_wrap); + } + if (env.common.findIdent("Builtin.Num.I128.to_u128_try")) |ident| { + try low_level_map.put(ident, .i128_to_u128_try); + } + if (env.common.findIdent("Builtin.Num.I128.to_f32")) |ident| { + try low_level_map.put(ident, .i128_to_f32); + } + if (env.common.findIdent("Builtin.Num.I128.to_f64")) |ident| { + try low_level_map.put(ident, .i128_to_f64); + } + if (env.common.findIdent("i128_to_dec_try_unsafe")) |ident| { + try low_level_map.put(ident, .i128_to_dec_try_unsafe); + } + + // F32 conversion operations + if (env.common.findIdent("Builtin.Num.F32.to_i8_trunc")) |ident| { + try low_level_map.put(ident, .f32_to_i8_trunc); + } + if (env.common.findIdent("f32_to_i8_try_unsafe")) |ident| { + try low_level_map.put(ident, .f32_to_i8_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F32.to_i16_trunc")) |ident| { + try low_level_map.put(ident, .f32_to_i16_trunc); + } + if (env.common.findIdent("f32_to_i16_try_unsafe")) |ident| { + try low_level_map.put(ident, .f32_to_i16_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F32.to_i32_trunc")) |ident| { + try low_level_map.put(ident, .f32_to_i32_trunc); + } + if (env.common.findIdent("f32_to_i32_try_unsafe")) |ident| { + try low_level_map.put(ident, .f32_to_i32_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F32.to_i64_trunc")) |ident| { + try low_level_map.put(ident, .f32_to_i64_trunc); + } + if (env.common.findIdent("f32_to_i64_try_unsafe")) |ident| { + try low_level_map.put(ident, .f32_to_i64_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F32.to_i128_trunc")) |ident| { + try low_level_map.put(ident, .f32_to_i128_trunc); + } + if (env.common.findIdent("f32_to_i128_try_unsafe")) |ident| { + try low_level_map.put(ident, .f32_to_i128_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F32.to_u8_trunc")) |ident| { + try low_level_map.put(ident, .f32_to_u8_trunc); + } + if (env.common.findIdent("f32_to_u8_try_unsafe")) |ident| { + try low_level_map.put(ident, .f32_to_u8_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F32.to_u16_trunc")) |ident| { + try low_level_map.put(ident, .f32_to_u16_trunc); + } + if (env.common.findIdent("f32_to_u16_try_unsafe")) |ident| { + try low_level_map.put(ident, .f32_to_u16_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F32.to_u32_trunc")) |ident| { + try low_level_map.put(ident, .f32_to_u32_trunc); + } + if (env.common.findIdent("f32_to_u32_try_unsafe")) |ident| { + try low_level_map.put(ident, .f32_to_u32_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F32.to_u64_trunc")) |ident| { + try low_level_map.put(ident, .f32_to_u64_trunc); + } + if (env.common.findIdent("f32_to_u64_try_unsafe")) |ident| { + try low_level_map.put(ident, .f32_to_u64_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F32.to_u128_trunc")) |ident| { + try low_level_map.put(ident, .f32_to_u128_trunc); + } + if (env.common.findIdent("f32_to_u128_try_unsafe")) |ident| { + try low_level_map.put(ident, .f32_to_u128_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F32.to_f64")) |ident| { + try low_level_map.put(ident, .f32_to_f64); + } + + // F64 conversion operations + if (env.common.findIdent("Builtin.Num.F64.to_i8_trunc")) |ident| { + try low_level_map.put(ident, .f64_to_i8_trunc); + } + if (env.common.findIdent("f64_to_i8_try_unsafe")) |ident| { + try low_level_map.put(ident, .f64_to_i8_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F64.to_i16_trunc")) |ident| { + try low_level_map.put(ident, .f64_to_i16_trunc); + } + if (env.common.findIdent("f64_to_i16_try_unsafe")) |ident| { + try low_level_map.put(ident, .f64_to_i16_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F64.to_i32_trunc")) |ident| { + try low_level_map.put(ident, .f64_to_i32_trunc); + } + if (env.common.findIdent("f64_to_i32_try_unsafe")) |ident| { + try low_level_map.put(ident, .f64_to_i32_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F64.to_i64_trunc")) |ident| { + try low_level_map.put(ident, .f64_to_i64_trunc); + } + if (env.common.findIdent("f64_to_i64_try_unsafe")) |ident| { + try low_level_map.put(ident, .f64_to_i64_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F64.to_i128_trunc")) |ident| { + try low_level_map.put(ident, .f64_to_i128_trunc); + } + if (env.common.findIdent("f64_to_i128_try_unsafe")) |ident| { + try low_level_map.put(ident, .f64_to_i128_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F64.to_u8_trunc")) |ident| { + try low_level_map.put(ident, .f64_to_u8_trunc); + } + if (env.common.findIdent("f64_to_u8_try_unsafe")) |ident| { + try low_level_map.put(ident, .f64_to_u8_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F64.to_u16_trunc")) |ident| { + try low_level_map.put(ident, .f64_to_u16_trunc); + } + if (env.common.findIdent("f64_to_u16_try_unsafe")) |ident| { + try low_level_map.put(ident, .f64_to_u16_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F64.to_u32_trunc")) |ident| { + try low_level_map.put(ident, .f64_to_u32_trunc); + } + if (env.common.findIdent("f64_to_u32_try_unsafe")) |ident| { + try low_level_map.put(ident, .f64_to_u32_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F64.to_u64_trunc")) |ident| { + try low_level_map.put(ident, .f64_to_u64_trunc); + } + if (env.common.findIdent("f64_to_u64_try_unsafe")) |ident| { + try low_level_map.put(ident, .f64_to_u64_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F64.to_u128_trunc")) |ident| { + try low_level_map.put(ident, .f64_to_u128_trunc); + } + if (env.common.findIdent("f64_to_u128_try_unsafe")) |ident| { + try low_level_map.put(ident, .f64_to_u128_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.F64.to_f32_wrap")) |ident| { + try low_level_map.put(ident, .f64_to_f32_wrap); + } + if (env.common.findIdent("f64_to_f32_try_unsafe")) |ident| { + try low_level_map.put(ident, .f64_to_f32_try_unsafe); + } + + // Dec conversion functions + if (env.common.findIdent("Builtin.Num.Dec.to_i8_trunc")) |ident| { + try low_level_map.put(ident, .dec_to_i8_trunc); + } + if (env.common.findIdent("dec_to_i8_try_unsafe")) |ident| { + try low_level_map.put(ident, .dec_to_i8_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.Dec.to_i16_trunc")) |ident| { + try low_level_map.put(ident, .dec_to_i16_trunc); + } + if (env.common.findIdent("dec_to_i16_try_unsafe")) |ident| { + try low_level_map.put(ident, .dec_to_i16_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.Dec.to_i32_trunc")) |ident| { + try low_level_map.put(ident, .dec_to_i32_trunc); + } + if (env.common.findIdent("dec_to_i32_try_unsafe")) |ident| { + try low_level_map.put(ident, .dec_to_i32_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.Dec.to_i64_trunc")) |ident| { + try low_level_map.put(ident, .dec_to_i64_trunc); + } + if (env.common.findIdent("dec_to_i64_try_unsafe")) |ident| { + try low_level_map.put(ident, .dec_to_i64_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.Dec.to_i128_trunc")) |ident| { + try low_level_map.put(ident, .dec_to_i128_trunc); + } + if (env.common.findIdent("dec_to_i128_try_unsafe")) |ident| { + try low_level_map.put(ident, .dec_to_i128_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.Dec.to_u8_trunc")) |ident| { + try low_level_map.put(ident, .dec_to_u8_trunc); + } + if (env.common.findIdent("dec_to_u8_try_unsafe")) |ident| { + try low_level_map.put(ident, .dec_to_u8_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.Dec.to_u16_trunc")) |ident| { + try low_level_map.put(ident, .dec_to_u16_trunc); + } + if (env.common.findIdent("dec_to_u16_try_unsafe")) |ident| { + try low_level_map.put(ident, .dec_to_u16_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.Dec.to_u32_trunc")) |ident| { + try low_level_map.put(ident, .dec_to_u32_trunc); + } + if (env.common.findIdent("dec_to_u32_try_unsafe")) |ident| { + try low_level_map.put(ident, .dec_to_u32_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.Dec.to_u64_trunc")) |ident| { + try low_level_map.put(ident, .dec_to_u64_trunc); + } + if (env.common.findIdent("dec_to_u64_try_unsafe")) |ident| { + try low_level_map.put(ident, .dec_to_u64_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.Dec.to_u128_trunc")) |ident| { + try low_level_map.put(ident, .dec_to_u128_trunc); + } + if (env.common.findIdent("dec_to_u128_try_unsafe")) |ident| { + try low_level_map.put(ident, .dec_to_u128_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.Dec.to_f32_wrap")) |ident| { + try low_level_map.put(ident, .dec_to_f32_wrap); + } + if (env.common.findIdent("dec_to_f32_try_unsafe")) |ident| { + try low_level_map.put(ident, .dec_to_f32_try_unsafe); + } + if (env.common.findIdent("Builtin.Num.Dec.to_f64")) |ident| { + try low_level_map.put(ident, .dec_to_f64); + } + + // Iterate through all defs and replace matching anno-only defs with low-level implementations + // NOTE: We copy def indices to a separate list first, because operations inside the loop + // may reallocate extra_data, which would invalidate any slice taken from it. + const all_defs_slice = env.store.sliceDefs(env.all_defs); + var def_indices = std.ArrayList(CIR.Def.Idx).empty; + defer def_indices.deinit(gpa); + try def_indices.appendSlice(gpa, all_defs_slice); + + for (def_indices.items) |def_idx| { + const def = env.store.getDef(def_idx); + const expr = env.store.getExpr(def.expr); + + // Check if this is an anno-only def (e_anno_only expression) + if (expr == .e_anno_only and def.annotation != null) { + // Get the identifier from the pattern + const pattern = env.store.getPattern(def.pattern); + if (pattern == .assign) { + const ident = pattern.assign.ident; + + // Check if this identifier matches a low-level operation + if (low_level_map.fetchRemove(ident)) |entry| { + const low_level_op = entry.value; + + // Get the number of parameters from the type annotation + // The annotation must be a function type for low-level operations + const annotation = env.store.getAnnotation(def.annotation.?); + const type_anno = env.store.getTypeAnno(annotation.anno); + const num_params: u32 = switch (type_anno) { + .@"fn" => |func| func.args.span.len, + else => std.debug.panic("Low-level operation {s} does not have a function type annotation", .{@tagName(low_level_op)}), + }; + + const patterns_start = env.store.scratchTop("patterns"); + var i: u32 = 0; + while (i < num_params) : (i += 1) { + var arg_name_buf: [16]u8 = undefined; + const arg_name = try std.fmt.bufPrint(&arg_name_buf, "_arg{d}", .{i}); + const arg_ident = env.common.findIdent(arg_name) orelse try env.common.insertIdent(gpa, base.Ident.for_text(arg_name)); + const arg_pattern_idx = try env.addPattern(.{ .assign = .{ .ident = arg_ident } }, base.Region.zero()); + try env.store.scratch.?.patterns.append(arg_pattern_idx); + } + const args_span = try env.store.patternSpanFrom(patterns_start); + + // Create an e_runtime_error body that crashes when the function is called + const error_msg_lit = try env.insertString("Low-level builtin not yet implemented in interpreter"); + const diagnostic_idx = try env.addDiagnostic(.{ .not_implemented = .{ + .feature = error_msg_lit, + .region = base.Region.zero(), + } }); + const body_idx = try env.addExpr(.{ .e_runtime_error = .{ .diagnostic = diagnostic_idx } }, base.Region.zero()); + + // Create e_low_level_lambda expression + const expr_idx = try env.addExpr(.{ .e_low_level_lambda = .{ + .op = low_level_op, + .args = args_span, + .body = body_idx, + } }, base.Region.zero()); + + // Now replace the e_anno_only expression with the e_low_level_lambda + // Def structure is stored in extra_data: + // extra_data[0] = pattern, extra_data[1] = expr, ... + // node.data_1 points to the start index in extra_data + const def_node_idx = @as(@TypeOf(env.store.nodes).Idx, @enumFromInt(@intFromEnum(def_idx))); + const def_node = env.store.nodes.get(def_node_idx); + const extra_start = def_node.data_1; + + // Update the expr field (at extra_start + 1) + env.store.extra_data.items.items[extra_start + 1] = @intFromEnum(expr_idx); + + // Track this replaced def index + try new_def_indices.append(gpa, def_idx); + } + } + } + } + + // Verify all low-level operations were found in the builtins + if (low_level_map.count() > 0) { + std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{}); + std.debug.print("ERROR: Low-level operations not found in Builtin.roc\n", .{}); + std.debug.print("=" ** 80 ++ "\n\n", .{}); + + std.debug.print("The following low-level operations were not found:\n", .{}); + var iter = low_level_map.iterator(); + while (iter.next()) |entry| { + const ident_text = env.getIdentText(entry.key_ptr.*); + const op_name = @tagName(entry.value_ptr.*); + std.debug.print(" - {s} (mapped to .{s})\n", .{ ident_text, op_name }); + } + std.debug.print("\nEither:\n", .{}); + std.debug.print(" 1. Remove the obsolete entry from the low_level_map in builtin_compiler/main.zig, OR\n", .{}); + std.debug.print(" 2. Add a standalone type annotation to Builtin.roc for it to match\n", .{}); + + std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{}); + std.debug.print("Builtin compiler exiting with error code due to {d} missing operation(s)\n", .{low_level_map.count()}); + std.debug.print("=" ** 80 ++ "\n", .{}); + + return error.LowLevelOperationsNotFound; + } + + return new_def_indices; +} + +fn readFileAllocPath(gpa: Allocator, path: []const u8) ![]u8 { + if (std.fs.path.isAbsolute(path)) { + var file = try std.fs.openFileAbsolute(path, .{}); + defer file.close(); + return try file.readToEndAlloc(gpa, max_builtin_bytes); + } + return try std.fs.cwd().readFileAlloc(gpa, path, max_builtin_bytes); +} + +/// Build-time compiler that compiles builtin .roc sources into serialized ModuleEnvs. +/// This runs during `zig build` on the host machine to generate .bin files +/// that get embedded into the final roc executable. +/// +/// The build system passes the absolute path to Builtin.roc as the first argument for cache tracking; +/// we honor that when present so the compiler works regardless of the current working directory. +pub fn main() !void { + var gpa_impl = std.heap.GeneralPurposeAllocator(.{}){}; + defer { + const leaked = gpa_impl.deinit(); + if (leaked == .leak) { + std.debug.print("WARNING: Memory leaked!\n", .{}); + } + } + const gpa = gpa_impl.allocator(); + + const args = try std.process.argsAlloc(gpa); + defer std.process.argsFree(gpa, args); + + // Prefer the absolute path provided by the build system, but fall back to the + // project-relative path so manual runs (e.g. `zig build run`) still succeed. + const builtin_src_path = if (args.len >= 2) args[1] else "src/build/roc/Builtin.roc"; + + // Read the Builtin.roc source file at runtime + // NOTE: We must free this source manually; CommonEnv.deinit() does not free the source. + const builtin_roc_source = try readFileAllocPath(gpa, builtin_src_path); + + // Compile Builtin.roc (it's completely self-contained) + const builtin_env = try compileModule( + gpa, + "Builtin", + builtin_roc_source, + builtin_src_path, + &.{}, // No module dependencies + null, // bool_stmt not available yet (will be found within Builtin) + null, // try_stmt not available yet (will be found within Builtin) + null, // str_stmt not available yet (will be found within Builtin) + ); + defer { + builtin_env.deinit(); + gpa.destroy(builtin_env); + gpa.free(builtin_roc_source); + } + + // Find nested type declarations in Builtin module + // These are nested inside Builtin's record extension (Builtin := [].{...}) + const bool_type_idx = try findTypeDeclaration(builtin_env, "Bool"); + const try_type_idx = try findTypeDeclaration(builtin_env, "Try"); + const dict_type_idx = try findTypeDeclaration(builtin_env, "Dict"); + const set_type_idx = try findTypeDeclaration(builtin_env, "Set"); + const str_type_idx = try findTypeDeclaration(builtin_env, "Str"); + const list_type_idx = try findTypeDeclaration(builtin_env, "List"); + const box_type_idx = try findTypeDeclaration(builtin_env, "Box"); + + // Find Utf8Problem nested inside Str (e.g., Builtin.Str.Utf8Problem) + const utf8_problem_type_idx = try findNestedTypeDeclaration(builtin_env, "Str", "Utf8Problem"); + + // Find numeric types nested inside Num (e.g., Builtin.Num.U8) + const u8_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "U8"); + const i8_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "I8"); + const u16_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "U16"); + const i16_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "I16"); + const u32_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "U32"); + const i32_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "I32"); + const u64_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "U64"); + const i64_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "I64"); + const u128_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "U128"); + const i128_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "I128"); + const dec_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "Dec"); + const f32_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "F32"); + const f64_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "F64"); + const numeral_type_idx = try findNestedTypeDeclaration(builtin_env, "Num", "Numeral"); + + // Look up idents for each type + // All types use fully-qualified names for consistent member lookup + // Top-level types: "Builtin.Bool", "Builtin.Str", etc. + // Nested types under Num: "Builtin.Num.U8", etc. + const bool_ident = builtin_env.common.findIdent("Builtin.Bool") orelse unreachable; + const try_ident = builtin_env.common.findIdent("Builtin.Try") orelse unreachable; + const dict_ident = builtin_env.common.findIdent("Builtin.Dict") orelse unreachable; + const set_ident = builtin_env.common.findIdent("Builtin.Set") orelse unreachable; + const str_ident = builtin_env.common.findIdent("Builtin.Str") orelse unreachable; + const list_ident = builtin_env.common.findIdent("Builtin.List") orelse unreachable; + const box_ident = builtin_env.common.findIdent("Builtin.Box") orelse unreachable; + const utf8_problem_ident = builtin_env.common.findIdent("Builtin.Str.Utf8Problem") orelse unreachable; + const u8_ident = builtin_env.common.findIdent("Builtin.Num.U8") orelse unreachable; + const i8_ident = builtin_env.common.findIdent("Builtin.Num.I8") orelse unreachable; + const u16_ident = builtin_env.common.findIdent("Builtin.Num.U16") orelse unreachable; + const i16_ident = builtin_env.common.findIdent("Builtin.Num.I16") orelse unreachable; + const u32_ident = builtin_env.common.findIdent("Builtin.Num.U32") orelse unreachable; + const i32_ident = builtin_env.common.findIdent("Builtin.Num.I32") orelse unreachable; + const u64_ident = builtin_env.common.findIdent("Builtin.Num.U64") orelse unreachable; + const i64_ident = builtin_env.common.findIdent("Builtin.Num.I64") orelse unreachable; + const u128_ident = builtin_env.common.findIdent("Builtin.Num.U128") orelse unreachable; + const i128_ident = builtin_env.common.findIdent("Builtin.Num.I128") orelse unreachable; + const dec_ident = builtin_env.common.findIdent("Builtin.Num.Dec") orelse unreachable; + const f32_ident = builtin_env.common.findIdent("Builtin.Num.F32") orelse unreachable; + const f64_ident = builtin_env.common.findIdent("Builtin.Num.F64") orelse unreachable; + const numeral_ident = builtin_env.common.findIdent("Builtin.Num.Numeral") orelse unreachable; + // Tag idents for Try type (Ok and Err) + const ok_ident = builtin_env.common.findIdent("Ok") orelse unreachable; + const err_ident = builtin_env.common.findIdent("Err") orelse unreachable; + + // Expose the types so they can be found by getExposedNodeIndexById (used for auto-imports) + // Note: These types are already in exposed_items from canonicalization, we just set their node indices + try builtin_env.common.setNodeIndexById(gpa, bool_ident, @intCast(@intFromEnum(bool_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, try_ident, @intCast(@intFromEnum(try_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, dict_ident, @intCast(@intFromEnum(dict_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, set_ident, @intCast(@intFromEnum(set_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, str_ident, @intCast(@intFromEnum(str_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, list_ident, @intCast(@intFromEnum(list_type_idx))); + + try builtin_env.common.setNodeIndexById(gpa, u8_ident, @intCast(@intFromEnum(u8_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, i8_ident, @intCast(@intFromEnum(i8_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, u16_ident, @intCast(@intFromEnum(u16_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, i16_ident, @intCast(@intFromEnum(i16_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, u32_ident, @intCast(@intFromEnum(u32_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, i32_ident, @intCast(@intFromEnum(i32_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, u64_ident, @intCast(@intFromEnum(u64_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, i64_ident, @intCast(@intFromEnum(i64_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, u128_ident, @intCast(@intFromEnum(u128_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, i128_ident, @intCast(@intFromEnum(i128_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, dec_ident, @intCast(@intFromEnum(dec_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, f32_ident, @intCast(@intFromEnum(f32_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, f64_ident, @intCast(@intFromEnum(f64_type_idx))); + try builtin_env.common.setNodeIndexById(gpa, numeral_ident, @intCast(@intFromEnum(numeral_type_idx))); + + // Create output directory + try std.fs.cwd().makePath("zig-out/builtins"); + + // Serialize the single Builtin module + try serializeModuleEnv(gpa, builtin_env, "zig-out/builtins/Builtin.bin"); + + // Create and serialize builtin indices + const builtin_indices = BuiltinIndices{ + // Statement indices + .bool_type = bool_type_idx, + .try_type = try_type_idx, + .dict_type = dict_type_idx, + .set_type = set_type_idx, + .str_type = str_type_idx, + .list_type = list_type_idx, + .box_type = box_type_idx, + .utf8_problem_type = utf8_problem_type_idx, + .u8_type = u8_type_idx, + .i8_type = i8_type_idx, + .u16_type = u16_type_idx, + .i16_type = i16_type_idx, + .u32_type = u32_type_idx, + .i32_type = i32_type_idx, + .u64_type = u64_type_idx, + .i64_type = i64_type_idx, + .u128_type = u128_type_idx, + .i128_type = i128_type_idx, + .dec_type = dec_type_idx, + .f32_type = f32_type_idx, + .f64_type = f64_type_idx, + .numeral_type = numeral_type_idx, + .bool_ident = bool_ident, + .try_ident = try_ident, + .dict_ident = dict_ident, + .set_ident = set_ident, + .str_ident = str_ident, + .list_ident = list_ident, + .box_ident = box_ident, + .utf8_problem_ident = utf8_problem_ident, + .u8_ident = u8_ident, + .i8_ident = i8_ident, + .u16_ident = u16_ident, + .i16_ident = i16_ident, + .u32_ident = u32_ident, + .i32_ident = i32_ident, + .u64_ident = u64_ident, + .i64_ident = i64_ident, + .u128_ident = u128_ident, + .i128_ident = i128_ident, + .dec_ident = dec_ident, + .f32_ident = f32_ident, + .f64_ident = f64_ident, + .numeral_ident = numeral_ident, + .ok_ident = ok_ident, + .err_ident = err_ident, + }; + + // Validate that BuiltinIndices contains all type declarations under Builtin + // This ensures BuiltinIndices stays in sync with the actual Builtin module content + try validateBuiltinIndicesCompleteness(builtin_env, builtin_indices); + + try serializeBuiltinIndices(builtin_indices, "zig-out/builtins/builtin_indices.bin"); +} + +/// Validates that BuiltinIndices contains all nominal type declarations in the Builtin module. +/// Iterates through all statements and ensures every s_nominal_decl is present in BuiltinIndices, +/// with the exception of "Num" which is a container type, not an auto-imported type. +fn validateBuiltinIndicesCompleteness(env: *const ModuleEnv, indices: BuiltinIndices) !void { + // Collect all statement indices from BuiltinIndices using reflection + // Only check Statement.Idx fields (skip Ident.Idx fields) + var indexed_stmts = std.AutoHashMap(CIR.Statement.Idx, void).init(std.heap.page_allocator); + defer indexed_stmts.deinit(); + + const fields = @typeInfo(BuiltinIndices).@"struct".fields; + inline for (fields) |field| { + if (field.type == CIR.Statement.Idx) { + const stmt_idx = @field(indices, field.name); + try indexed_stmts.put(stmt_idx, {}); + } + } + + // Check all nominal type declarations in the Builtin module + const all_stmts = env.store.sliceStatements(env.all_statements); + for (all_stmts) |stmt_idx| { + const stmt = env.store.getStatement(stmt_idx); + switch (stmt) { + .s_nominal_decl => |decl| { + const header = env.store.getTypeHeader(decl.header); + const ident_text = env.getIdentText(header.name); + + // Skip container types that are not auto-imported types + if (std.mem.eql(u8, ident_text, "Builtin") or + std.mem.eql(u8, ident_text, "Builtin.Num")) + { + continue; + } + + // Every other nominal type should be in BuiltinIndices + if (!indexed_stmts.contains(stmt_idx)) { + std.debug.print("ERROR: Type '{s}' (stmt_idx={d}) is not in BuiltinIndices!\n", .{ + ident_text, + @intFromEnum(stmt_idx), + }); + std.debug.print("Add this type to BuiltinIndices in CIR.zig and builtin_compiler/main.zig\n", .{}); + return error.BuiltinIndicesIncomplete; + } + }, + else => continue, + } + } +} + +const ModuleDep = struct { + name: []const u8, + env: *const ModuleEnv, +}; + +fn compileModule( + gpa: Allocator, + module_name: []const u8, + source: []const u8, + source_path: []const u8, + deps: []const ModuleDep, + bool_stmt_opt: ?CIR.Statement.Idx, + try_stmt_opt: ?CIR.Statement.Idx, + str_stmt_opt: ?CIR.Statement.Idx, +) !*ModuleEnv { + // This follows the pattern from TestEnv.init() in src/check/test/TestEnv.zig + + // 1. Create ModuleEnv + var module_env = try gpa.create(ModuleEnv); + errdefer gpa.destroy(module_env); + + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + + module_env.* = try ModuleEnv.init(gpa, source); + errdefer module_env.deinit(); + + module_env.module_name = module_name; + try module_env.common.calcLineStarts(gpa); + + // 2. Create common idents (needed for type checking) + const module_ident = try module_env.insertIdent(base.Ident.for_text(module_name)); + + // Use provided bool_stmt, try_stmt, and str_stmt if available, otherwise use undefined + // For Builtin module, these will be found after canonicalization and updated before type checking + var builtin_ctx: Check.BuiltinContext = .{ + .module_name = module_ident, + .bool_stmt = bool_stmt_opt orelse undefined, + .try_stmt = try_stmt_opt orelse undefined, + .str_stmt = str_stmt_opt orelse undefined, + .builtin_module = null, + .builtin_indices = null, + }; + + // 3. Parse + var parse_ast = try gpa.create(parse.AST); + defer { + parse_ast.deinit(gpa); + gpa.destroy(parse_ast); + } + + parse_ast.* = try parse.parse(&module_env.common, gpa); + parse_ast.store.emptyScratch(); + + // Check for parse errors + if (parse_ast.hasErrors()) { + const stderr = stderrWriter(); + const palette = reporting.ColorUtils.getPaletteForConfig(reporting.ReportingConfig.initColorTerminal()); + const config = reporting.ReportingConfig.initColorTerminal(); + + // Render tokenize diagnostics + for (parse_ast.tokenize_diagnostics.items) |diag| { + var report = parse_ast.tokenizeDiagnosticToReport(diag, gpa, source_path) catch |err| { + std.debug.print("Error creating tokenize diagnostic report: {}\n", .{err}); + continue; + }; + defer report.deinit(); + reporting.renderReportToTerminal(&report, stderr, palette, config) catch |err| { + std.debug.print("Error rendering tokenize diagnostic: {}\n", .{err}); + }; + } + + // Render parse diagnostics + for (parse_ast.parse_diagnostics.items) |diag| { + var report = parse_ast.parseDiagnosticToReport(&module_env.common, diag, gpa, source_path) catch |err| { + std.debug.print("Error creating parse diagnostic report: {}\n", .{err}); + continue; + }; + defer report.deinit(); + reporting.renderReportToTerminal(&report, stderr, palette, config) catch |err| { + std.debug.print("Error rendering parse diagnostic: {}\n", .{err}); + }; + } + + flushStderr(); + return error.ParseError; + } + + // 4. Canonicalize + try module_env.initCIRFields(module_name); + + var can_result = try gpa.create(Can); + defer { + can_result.deinit(); + gpa.destroy(can_result); + } + + // When compiling Builtin itself, pass null for module_envs so setupAutoImportedBuiltinTypes doesn't run + can_result.* = try Can.init(module_env, parse_ast, null); + + try can_result.canonicalizeFile(); + try can_result.validateForChecking(); + + // Check for canonicalization errors + const can_diagnostics = try module_env.getDiagnostics(); + defer gpa.free(can_diagnostics); + if (can_diagnostics.len > 0) { + const stderr = stderrWriter(); + const palette = reporting.ColorUtils.getPaletteForConfig(reporting.ReportingConfig.initColorTerminal()); + const config = reporting.ReportingConfig.initColorTerminal(); + + for (can_diagnostics) |diag| { + var report = module_env.diagnosticToReport(diag, gpa, source_path) catch |err| { + std.debug.print("Error creating canonicalization diagnostic report: {}\n", .{err}); + continue; + }; + defer report.deinit(); + reporting.renderReportToTerminal(&report, stderr, palette, config) catch |err| { + std.debug.print("Error rendering canonicalization diagnostic: {}\n", .{err}); + }; + } + + flushStderr(); + return error.CanonicalizeError; + } + + // 5.5. Transform low-level operations (must happen before type checking) + // For the Builtin module, transform annotation-only defs into low-level operations + if (std.mem.eql(u8, module_name, "Builtin")) { + // Transform annotation-only defs and get the list of new def indices + var new_def_indices = try replaceStrIsEmptyWithLowLevel(module_env); + defer new_def_indices.deinit(gpa); + + if (new_def_indices.items.len > 0) { + // Rebuild the dependency graph and evaluation order to include the updated defs + const DependencyGraph = @import("can").DependencyGraph; + var graph = try DependencyGraph.buildDependencyGraph( + module_env, + module_env.all_defs, + gpa, + ); + defer graph.deinit(); + + const eval_order = try DependencyGraph.computeSCCs(&graph, gpa); + // Free the old evaluation order if it exists + if (module_env.evaluation_order) |old_order| { + old_order.deinit(); + gpa.destroy(old_order); + } + const eval_order_ptr = try gpa.create(DependencyGraph.EvaluationOrder); + eval_order_ptr.* = eval_order; + module_env.evaluation_order = eval_order_ptr; + } + + // Find Bool, Try, and Str statements before type checking + // When compiling Builtin, bool_stmt, try_stmt, and str_stmt are initially undefined, + // but they must be set before type checking begins + const found_bool_stmt = findTypeDeclaration(module_env, "Bool") catch { + std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{}); + std.debug.print("ERROR: Could not find Bool type in Builtin module\n", .{}); + std.debug.print("=" ** 80 ++ "\n", .{}); + std.debug.print("The Bool type declaration is required for type checking.\n", .{}); + std.debug.print("=" ** 80 ++ "\n", .{}); + return error.TypeDeclarationNotFound; + }; + const found_try_stmt = findTypeDeclaration(module_env, "Try") catch { + std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{}); + std.debug.print("ERROR: Could not find Try type in Builtin module\n", .{}); + std.debug.print("=" ** 80 ++ "\n", .{}); + std.debug.print("The Try type declaration is required for type checking.\n", .{}); + std.debug.print("=" ** 80 ++ "\n", .{}); + return error.TypeDeclarationNotFound; + }; + const found_str_stmt = findTypeDeclaration(module_env, "Str") catch { + std.debug.print("\n" ++ "=" ** 80 ++ "\n", .{}); + std.debug.print("ERROR: Could not find Str type in Builtin module\n", .{}); + std.debug.print("=" ** 80 ++ "\n", .{}); + std.debug.print("The Str type declaration is required for type checking.\n", .{}); + std.debug.print("=" ** 80 ++ "\n", .{}); + return error.TypeDeclarationNotFound; + }; + + // Update builtin_ctx with the found statement indices + builtin_ctx.bool_stmt = found_bool_stmt; + builtin_ctx.try_stmt = found_try_stmt; + builtin_ctx.str_stmt = found_str_stmt; + } + + // 6. Type check + // Build the list of other modules for type checking + var imported_envs = std.ArrayList(*const ModuleEnv).empty; + defer imported_envs.deinit(gpa); + + // Add dependencies + for (deps) |dep| { + try imported_envs.append(gpa, dep.env); + } + + var module_envs = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(gpa); + defer module_envs.deinit(); + + var checker = try Check.init( + gpa, + &module_env.types, + module_env, + imported_envs.items, + &module_envs, + &module_env.store.regions, + builtin_ctx, + ); + defer checker.deinit(); + + try checker.checkFile(); + + // Check for type errors + if (checker.problems.problems.items.len > 0) { + const stderr = stderrWriter(); + const palette = reporting.ColorUtils.getPaletteForConfig(reporting.ReportingConfig.initColorTerminal()); + const config = reporting.ReportingConfig.initColorTerminal(); + + const problem = check.problem; + var report_builder = problem.ReportBuilder.init( + gpa, + module_env, + module_env, + &checker.snapshots, + source_path, + imported_envs.items, + &checker.import_mapping, + ); + defer report_builder.deinit(); + + for (0..checker.problems.len()) |i| { + const problem_idx: problem.Problem.Idx = @enumFromInt(i); + const prob = checker.problems.get(problem_idx); + var report = report_builder.build(prob) catch |err| { + std.debug.print("Error creating type problem report: {}\n", .{err}); + continue; + }; + defer report.deinit(); + reporting.renderReportToTerminal(&report, stderr, palette, config) catch |err| { + std.debug.print("Error rendering type problem: {}\n", .{err}); + }; + } + + flushStderr(); + return error.TypeCheckError; + } + + return module_env; +} + +fn serializeModuleEnv( + gpa: Allocator, + env: *const ModuleEnv, + output_path: []const u8, +) !void { + // This follows the pattern from module_env_test.zig + + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + // Create output file + const file = try std.fs.cwd().createFile(output_path, .{ .read = true }); + defer file.close(); + + // Serialize using CompactWriter + var writer = collections.CompactWriter.init(); + defer writer.deinit(arena_alloc); + + const serialized = try writer.appendAlloc(arena_alloc, ModuleEnv.Serialized); + try serialized.serialize(env, arena_alloc, &writer); + + // Write to file + try writer.writeGather(arena_alloc, file); +} + +/// Get the ident index from a type declaration statement +fn getTypeIdent(env: *const ModuleEnv, stmt_idx: CIR.Statement.Idx) base.Ident.Idx { + const stmt = env.store.getStatement(stmt_idx); + const header = env.store.getTypeHeader(stmt.s_nominal_decl.header); + return header.name; +} + +/// Find a type declaration by name in a compiled module +/// Returns the statement index of the type declaration +/// For builtin_compiler, types are always in all_statements (not builtin_statements) +/// because we're compiling Builtin.roc itself, not importing from it. +fn findTypeDeclaration(env: *const ModuleEnv, type_name: []const u8) !CIR.Statement.Idx { + // Construct the qualified name (e.g., "Builtin.Bool") + // Types in nested declarations are stored with their full qualified names + var qualified_name_buf: [256]u8 = undefined; + const qualified_name = try std.fmt.bufPrint(&qualified_name_buf, "{s}.{s}", .{ env.module_name, type_name }); + + // Search in all_statements (where Builtin.roc's own types are stored) + const all_stmts = env.store.sliceStatements(env.all_statements); + for (all_stmts) |stmt_idx| { + const stmt = env.store.getStatement(stmt_idx); + switch (stmt) { + .s_nominal_decl => |decl| { + const header = env.store.getTypeHeader(decl.header); + const ident_idx = header.name; + const ident_text = env.getIdentText(ident_idx); + if (std.mem.eql(u8, ident_text, qualified_name)) { + return stmt_idx; + } + }, + else => continue, + } + } + + return error.TypeDeclarationNotFound; +} + +/// Find a nested type declaration by parent and type name in a compiled module +/// For example, findNestedTypeDeclaration(env, "Num", "U8") finds "Builtin.Num.U8" +/// Returns the statement index of the type declaration +fn findNestedTypeDeclaration(env: *const ModuleEnv, parent_name: []const u8, type_name: []const u8) !CIR.Statement.Idx { + // Construct the qualified name (e.g., "Builtin.Num.U8") + var qualified_name_buf: [256]u8 = undefined; + const qualified_name = try std.fmt.bufPrint(&qualified_name_buf, "{s}.{s}.{s}", .{ env.module_name, parent_name, type_name }); + + // Search in all_statements (where Builtin.roc's own types are stored) + const all_stmts = env.store.sliceStatements(env.all_statements); + for (all_stmts) |stmt_idx| { + const stmt = env.store.getStatement(stmt_idx); + switch (stmt) { + .s_nominal_decl => |decl| { + const header = env.store.getTypeHeader(decl.header); + const ident_idx = header.name; + const ident_text = env.getIdentText(ident_idx); + if (std.mem.eql(u8, ident_text, qualified_name)) { + return stmt_idx; + } + }, + else => continue, + } + } + + return error.TypeDeclarationNotFound; +} + +/// Serialize BuiltinIndices to a binary file +fn serializeBuiltinIndices( + indices: BuiltinIndices, + output_path: []const u8, +) !void { + // Create output file + const file = try std.fs.cwd().createFile(output_path, .{}); + defer file.close(); + + // Write the struct directly as binary data + // This is a simple struct with two u32 fields, so we can write it directly + try file.writeAll(std.mem.asBytes(&indices)); +} diff --git a/src/build/glibc_stub.zig b/src/build/glibc_stub.zig new file mode 100644 index 0000000000..ab190fc61f --- /dev/null +++ b/src/build/glibc_stub.zig @@ -0,0 +1,146 @@ +//! GNU libc stub generation for test platforms + +const std = @import("std"); + +/// Generate assembly stub with essential libc symbols +pub fn generateComprehensiveStub( + writer: anytype, + target_arch: std.Target.Cpu.Arch, +) !void { + const ptr_width: u32 = switch (target_arch) { + .x86_64, .aarch64 => 8, + else => 4, + }; + + try writer.writeAll(".text\n"); + + // Generate __sysctl symbol + try writer.print(".balign 8\n.globl __sysctl\n.type __sysctl, %function\n__sysctl:", .{}); + switch (target_arch) { + .x86_64 => try writer.writeAll(" xor %rax, %rax\n ret\n\n"), + .aarch64 => try writer.writeAll(" mov x0, #0\n ret\n\n"), + else => try writer.writeAll(" ret\n\n"), + } + + // Essential libc symbols that must be present for linking + // These are resolved at runtime from real glibc + const essential_symbols = [_][]const u8{ + // Core libc + "__libc_start_main", + "abort", + "getauxval", + "__tls_get_addr", // Thread-local storage + "__errno_location", // Thread-safe errno access + // Memory operations + "memcpy", + "memmove", + "mmap", + "mmap64", + "munmap", + "mremap", + "msync", + // Used by Zig's CAllocator + "malloc", + "calloc", + "realloc", + "free", + "posix_memalign", + "malloc_usable_size", + // File I/O + "close", + "read", + "write", + "readv", + "writev", + "openat64", + "lseek64", + "pread64", + "pwritev64", + "flock", + "copy_file_range", + "sendfile64", + // Path operations + "realpath", + "readlink", + // Environment + "getenv", + "isatty", + "sysconf", // System configuration (page size, etc.) + // Signal handling + "sigaction", + "sigemptyset", + // Dynamic linker + "dl_iterate_phdr", + "getcontext", + // Math functions + "fmod", + "fmodf", + "trunc", + "truncf", + }; + + for (essential_symbols) |symbol| { + try writer.print(".balign 8\n.globl {s}\n.type {s}, %function\n{s}:\n", .{ symbol, symbol, symbol }); + + if (std.mem.eql(u8, symbol, "abort")) { + // abort should exit with code 1 + switch (target_arch) { + .x86_64 => try writer.writeAll(" mov $1, %rdi\n mov $60, %rax\n syscall\n\n"), + .aarch64 => try writer.writeAll(" mov x0, #1\n mov x8, #93\n svc #0\n\n"), + else => try writer.writeAll(" ret\n\n"), + } + } else { + // Other symbols return 0 or are no-ops (resolved at runtime) + switch (target_arch) { + .x86_64 => try writer.writeAll(" xor %rax, %rax\n ret\n\n"), + .aarch64 => try writer.writeAll(" mov x0, #0\n ret\n\n"), + else => try writer.writeAll(" ret\n\n"), + } + } + } + + // Add data section + try writer.writeAll(".data\n"); + try writer.print("_IO_stdin_used: ", .{}); + if (ptr_width == 8) { + try writer.writeAll(".quad 1\n"); + } else { + try writer.writeAll(".long 1\n"); + } + + // environ is a global variable (char **environ) + try writer.writeAll(".globl environ\n.type environ, %object\nenviron: "); + if (ptr_width == 8) { + try writer.writeAll(".quad 0\n"); + } else { + try writer.writeAll(".long 0\n"); + } +} + +/// Compile assembly stub to shared library using Zig's build system +pub fn compileAssemblyStub( + b: *std.Build, + asm_path: std.Build.LazyPath, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, +) *std.Build.Step.Compile { + // Create a dynamic (shared) library + const lib = b.addLibrary(.{ + .name = "c", + .linkage = .dynamic, // replaces addSharedLibrary(...) + .version = .{ .major = 6, .minor = 0, .patch = 0 }, + .root_module = b.createModule(.{ + .target = target, + .optimize = optimize, + }), + }); + // Add the assembly file as a source + lib.addAssemblyFile(asm_path); + + // Allow unresolved symbols at link time + lib.linker_allow_shlib_undefined = true; + + // Shared libraries should not be PIE + lib.pie = false; + return lib; +} diff --git a/src/build/modules.zig b/src/build/modules.zig index 85a25411f0..fa1c197f5d 100644 --- a/src/build/modules.zig +++ b/src/build/modules.zig @@ -1,4 +1,7 @@ +//! Build system utilities for configuring Zig modules with test filtering and dependency management. + const std = @import("std"); +const builtin = @import("builtin"); const Build = std.Build; const Module = Build.Module; const Step = Build.Step; @@ -6,16 +9,277 @@ const OptimizeMode = std.builtin.OptimizeMode; const ResolvedTarget = std.Build.ResolvedTarget; const Dependency = std.Build.Dependency; +const FilterInjection = struct { + filters: []const []const u8, + forced_count: usize, +}; + +const wrapper_scan_max_bytes = 16 * 1024 * 1024; + +fn filtersContain(haystack: []const []const u8, needle: []const u8) bool { + for (haystack) |item| { + if (std.mem.eql(u8, item, needle)) return true; + } + return false; +} + +fn aggregatorFilters(module_type: ModuleType) []const []const u8 { + return switch (module_type) { + .base => &.{"base tests"}, + .collections => &.{"collections tests"}, + .builtins => &.{"builtins tests"}, + .compile => &.{"compile tests"}, + .can => &.{"compile tests"}, + .check => &.{"check tests"}, + .parse => &.{"parser tests"}, + .layout => &.{"layout tests"}, + .eval => &.{"eval tests"}, + .ipc => &.{"ipc tests"}, + .repl => &.{"repl tests"}, + .fmt => &.{"fmt tests"}, + else => &.{}, + }; +} + +const FileToScan = struct { + path: []const u8, + include_imports: bool, +}; + +// Count `test { ... }` blocks (no names) so filtered runs can subtract the +// wrappers they inevitably execute even when Zig test filters are set. +fn wrapperTestCount(b: *Build, module_type: ModuleType, module: *Module) usize { + const lazy_path = module.root_source_file orelse return 0; + const root_file_path = lazy_path.getPath(b); + const aggregator_names = aggregatorFilters(module_type); + const has_aggregators = aggregator_names.len != 0; + + var arena = std.heap.ArenaAllocator.init(b.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var pending = std.ArrayList(FileToScan).empty; + defer pending.deinit(allocator); + var seen = std.StringHashMap(void).init(allocator); + defer seen.deinit(); + + const root_copy = allocator.dupe(u8, root_file_path) catch @panic("OOM"); + pending.append(allocator, .{ + .path = root_copy, + .include_imports = has_aggregators, + }) catch @panic("OOM"); + seen.put(root_copy, {}) catch @panic("OOM"); + + var total: usize = 0; + while (pending.items.len != 0) { + const entry = pending.items[pending.items.len - 1]; + pending.items.len -= 1; + total += scanFileForWrappers( + allocator, + entry, + &pending, + &seen, + has_aggregators, + ); + } + + return total; +} + +fn scanFileForWrappers( + allocator: std.mem.Allocator, + entry: FileToScan, + pending: *std.ArrayList(FileToScan), + seen: *std.StringHashMap(void), + has_aggregators: bool, +) usize { + const path = entry.path; + const source = std.fs.cwd().readFileAllocOptions( + allocator, + path, + wrapper_scan_max_bytes, + null, + .@"1", + 0, + ) catch |err| { + std.log.warn( + "Failed to read {s} while counting unnamed tests: {s}", + .{ path, @errorName(err) }, + ); + return 0; + }; + + var tree = std.zig.Ast.parse(allocator, source, .zig) catch |err| { + std.log.warn( + "Failed to parse {s} while counting unnamed tests: {s}", + .{ path, @errorName(err) }, + ); + return 0; + }; + defer tree.deinit(allocator); + + const tags = tree.nodes.items(.tag); + const all_data = tree.nodes.items(.data); + + var unnamed: usize = 0; + for (tags, all_data) |tag, data| { + if (tag == .test_decl and data.opt_token_and_node[0] == .none) { + unnamed += 1; + } + } + + if (entry.include_imports and has_aggregators) { + collectAggregatorImports(allocator, source, path, pending, seen); + } + + return unnamed; +} + +fn collectAggregatorImports( + allocator: std.mem.Allocator, + source: []const u8, + current_path: []const u8, + pending: *std.ArrayList(FileToScan), + seen: *std.StringHashMap(void), +) void { + const pattern = "std.testing.refAllDecls(@import(\""; + var search_index: usize = 0; + const current_dir = std.fs.path.dirname(current_path) orelse "."; + + while (std.mem.indexOfPos(u8, source, search_index, pattern)) |match_pos| { + const literal_start = match_pos + pattern.len; + var cursor = literal_start; + while (cursor < source.len) : (cursor += 1) { + if (source[cursor] == '\\') { + cursor += 1; + continue; + } + if (source[cursor] == '"') break; + } + if (cursor >= source.len) break; + + const literal_bytes = source[literal_start..cursor]; + const quoted = std.fmt.allocPrint(allocator, "\"{s}\"", .{literal_bytes}) catch break; + const import_rel = std.zig.string_literal.parseAlloc(allocator, quoted) catch |err| { + std.log.warn( + "Failed to parse aggregator import in {s}: {s}", + .{ current_path, @errorName(err) }, + ); + search_index = cursor + 1; + continue; + }; + + const resolved = resolveImportPath(allocator, current_dir, import_rel) catch |err| { + std.log.warn( + "Failed to resolve aggregator import {s} from {s}: {s}", + .{ import_rel, current_path, @errorName(err) }, + ); + search_index = cursor + 1; + continue; + }; + + if (seen.contains(resolved)) { + search_index = cursor + 1; + continue; + } + + seen.put(resolved, {}) catch @panic("OOM"); + pending.append(allocator, .{ + .path = resolved, + .include_imports = false, + }) catch @panic("OOM"); + + search_index = cursor + 1; + } +} + +fn resolveImportPath( + allocator: std.mem.Allocator, + current_dir: []const u8, + import_rel: []const u8, +) ![]const u8 { + if (std.fs.path.isAbsolute(import_rel)) { + return std.fs.path.resolve(allocator, &.{import_rel}); + } + return std.fs.path.resolve(allocator, &.{ current_dir, import_rel }); +} + +// Keep module-level aggregator tests (e.g. "check tests") when user passes +// a filter for an inner test: we must still run the aggregator so that +// std.testing.refAllDecls brings the inner test into the build. +fn ensureAggregatorFilters( + b: *Build, + module_type: ModuleType, + base_filters: []const []const u8, +) FilterInjection { + if (base_filters.len == 0) { + return .{ .filters = base_filters, .forced_count = 0 }; + } + + const aggregators = aggregatorFilters(module_type); + if (aggregators.len == 0) { + return .{ .filters = base_filters, .forced_count = 0 }; + } + + var missing: usize = 0; + for (aggregators) |agg| { + if (!filtersContain(base_filters, agg)) { + missing += 1; + } + } + if (missing == 0) { + return .{ .filters = base_filters, .forced_count = 0 }; + } + + const combined = b.allocator.alloc([]const u8, base_filters.len + missing) catch + @panic("OOM while applying aggregator filters"); + for (combined[0..base_filters.len], base_filters) |*dest, src| { + dest.* = src; + } + + var next = base_filters.len; + var added: usize = 0; + for (aggregators) |agg| { + if (!filtersContain(base_filters, agg)) { + combined[next] = agg; + next += 1; + added += 1; + } + } + + return .{ + .filters = combined, + .forced_count = added, + }; +} + +fn targetMatchesHost(target: ResolvedTarget) bool { + return target.result.os.tag == builtin.target.os.tag and + target.result.cpu.arch == builtin.target.cpu.arch and + target.result.abi == builtin.target.abi; +} + /// Represents a test module with its compilation and execution steps. pub const ModuleTest = struct { test_step: *Step.Compile, run_step: *Step.Run, }; +/// Bundles the per-module test steps with accounting for forced passes (aggregators + +/// unnamed wrappers) so callers can correct the reported totals. +pub const ModuleTestsResult = struct { + /// Compile/run steps for each module's tests, in creation order. + tests: [20]ModuleTest, + /// Number of synthetic passes the summary must subtract when filters were injected. + /// Includes aggregator ensures and unconditional wrapper tests. + forced_passes: usize, +}; + /// Enumerates the different modules in the Roc compiler codebase. pub const ModuleType = enum { collections, base, + roc_src, types, builtins, compile, @@ -35,31 +299,34 @@ pub const ModuleType = enum { bundle, unbundle, base58, + lsp, /// Returns the dependencies for this module type pub fn getDependencies(self: ModuleType) []const ModuleType { return switch (self) { .build_options => &.{}, - .builtins => &.{}, + .builtins => &.{.tracy}, .fs => &.{}, - .tracy => &.{ .build_options, .builtins }, + .tracy => &.{.build_options}, .collections => &.{}, - .base => &.{.collections}, - .types => &.{ .base, .collections }, + .base => &.{ .collections, .builtins }, + .roc_src => &.{}, + .types => &.{ .tracy, .base, .collections }, .reporting => &.{ .collections, .base }, .parse => &.{ .tracy, .collections, .base, .reporting }, - .can => &.{ .tracy, .builtins, .collections, .types, .base, .parse, .reporting }, + .can => &.{ .tracy, .builtins, .collections, .types, .base, .parse, .reporting, .build_options }, .check => &.{ .tracy, .builtins, .collections, .base, .parse, .types, .can, .reporting }, - .layout => &.{ .collections, .base, .types, .builtins, .can }, - .eval => &.{ .collections, .base, .types, .builtins, .parse, .can, .check, .layout, .build_options }, - .compile => &.{ .tracy, .build_options, .fs, .builtins, .collections, .base, .types, .parse, .can, .check, .reporting, .layout, .eval }, + .layout => &.{ .tracy, .collections, .base, .types, .builtins, .can }, + .eval => &.{ .tracy, .collections, .base, .types, .builtins, .parse, .can, .check, .layout, .build_options, .reporting }, + .compile => &.{ .tracy, .build_options, .fs, .builtins, .collections, .base, .types, .parse, .can, .check, .reporting, .layout, .eval, .unbundle }, .ipc => &.{}, - .repl => &.{ .base, .compile, .parse, .types, .can, .check, .builtins, .layout, .eval }, + .repl => &.{ .base, .collections, .compile, .parse, .types, .can, .check, .builtins, .layout, .eval }, .fmt => &.{ .base, .parse, .collections, .can, .fs, .tracy }, .watch => &.{.build_options}, - .bundle => &.{ .base, .collections, .base58 }, + .bundle => &.{ .base, .collections, .base58, .unbundle }, .unbundle => &.{ .base, .collections, .base58 }, .base58 => &.{}, + .lsp => &.{ .compile, .reporting, .build_options, .fs }, }; } }; @@ -68,6 +335,7 @@ pub const ModuleType = enum { pub const RocModules = struct { collections: *Module, base: *Module, + roc_src: *Module, types: *Module, builtins: *Module, compile: *Module, @@ -87,6 +355,8 @@ pub const RocModules = struct { bundle: *Module, unbundle: *Module, base58: *Module, + lsp: *Module, + roc_target: *Module, pub fn create(b: *Build, build_options_step: *Step.Options, zstd: ?*Dependency) RocModules { const self = RocModules{ @@ -95,6 +365,7 @@ pub const RocModules = struct { .{ .root_source_file = b.path("src/collections/mod.zig") }, ), .base = b.addModule("base", .{ .root_source_file = b.path("src/base/mod.zig") }), + .roc_src = b.addModule("roc_src", .{ .root_source_file = b.path("src/roc_src/mod.zig") }), .types = b.addModule("types", .{ .root_source_file = b.path("src/types/mod.zig") }), .builtins = b.addModule("builtins", .{ .root_source_file = b.path("src/builtins/mod.zig") }), .compile = b.addModule("compile", .{ .root_source_file = b.path("src/compile/mod.zig") }), @@ -117,13 +388,15 @@ pub const RocModules = struct { .bundle = b.addModule("bundle", .{ .root_source_file = b.path("src/bundle/mod.zig") }), .unbundle = b.addModule("unbundle", .{ .root_source_file = b.path("src/unbundle/mod.zig") }), .base58 = b.addModule("base58", .{ .root_source_file = b.path("src/base58/mod.zig") }), + .lsp = b.addModule("lsp", .{ .root_source_file = b.path("src/lsp/mod.zig") }), + .roc_target = b.addModule("roc_target", .{ .root_source_file = b.path("src/target/mod.zig") }), }; // Link zstd to bundle module if available (it's unsupported on wasm32, so don't link it) + // Note: unbundle uses Zig's stdlib zstd for WASM compatibility if (zstd) |z| { self.bundle.linkLibrary(z.artifact("zstd")); } - // Note: unbundle module uses Zig's std zstandard, so doesn't need C library // Setup module dependencies using our generic helper self.setupModuleDependencies(); @@ -154,6 +427,7 @@ pub const RocModules = struct { .bundle, .unbundle, .base58, + .lsp, }; // Setup dependencies for each module @@ -187,6 +461,7 @@ pub const RocModules = struct { step.root_module.addImport("repl", self.repl); step.root_module.addImport("fmt", self.fmt); step.root_module.addImport("watch", self.watch); + step.root_module.addImport("lsp", self.lsp); // Don't add bundle module for WASM targets (zstd C library not available) if (step.rootModuleTarget().cpu.arch != .wasm32) { @@ -195,6 +470,7 @@ pub const RocModules = struct { step.root_module.addImport("unbundle", self.unbundle); step.root_module.addImport("base58", self.base58); + step.root_module.addImport("roc_target", self.roc_target); } pub fn addAllToTest(self: RocModules, step: *Step.Compile) void { @@ -206,6 +482,7 @@ pub const RocModules = struct { return switch (module_type) { .collections => self.collections, .base => self.base, + .roc_src => self.roc_src, .types => self.types, .builtins => self.builtins, .compile => self.compile, @@ -225,6 +502,7 @@ pub const RocModules = struct { .bundle => self.bundle, .unbundle => self.unbundle, .base58 => self.base58, + .lsp => self.lsp, }; } @@ -237,7 +515,14 @@ pub const RocModules = struct { } } - pub fn createModuleTests(self: RocModules, b: *Build, target: ResolvedTarget, optimize: OptimizeMode, zstd: ?*Dependency) [19]ModuleTest { + pub fn createModuleTests( + self: RocModules, + b: *Build, + target: ResolvedTarget, + optimize: OptimizeMode, + zstd: ?*Dependency, + test_filters: []const []const u8, + ) ModuleTestsResult { const test_configs = [_]ModuleType{ .collections, .base, @@ -258,26 +543,36 @@ pub const RocModules = struct { .bundle, .unbundle, .base58, + .lsp, }; var tests: [test_configs.len]ModuleTest = undefined; + var forced_passes: usize = 0; inline for (test_configs, 0..) |module_type, i| { const module = self.getModule(module_type); + const filter_injection = ensureAggregatorFilters(b, module_type, test_filters); + forced_passes += filter_injection.forced_count; + if (test_filters.len != 0) { + const wrappers = wrapperTestCount(b, module_type, module); + forced_passes += wrappers; + } const test_step = b.addTest(.{ - .name = b.fmt("{s}_test", .{@tagName(module_type)}), - .root_source_file = module.root_source_file.?, - .target = target, - .optimize = optimize, - // IPC module needs libc for mmap, munmap, close on POSIX systems - // Bundle module needs libc for zstd - // Unbundle module doesn't need libc (uses Zig's std zstandard) - .link_libc = (module_type == .ipc or module_type == .bundle), + .name = b.fmt("{s}", .{@tagName(module_type)}), + .root_module = b.createModule(.{ + .root_source_file = module.root_source_file.?, + .target = target, + .optimize = optimize, + // IPC module needs libc for mmap, munmap, close on POSIX systems + // Bundle module needs libc for C zstd (unbundle uses stdlib zstd) + .link_libc = (module_type == .ipc or module_type == .bundle), + }), + .filters = filter_injection.filters, }); // Watch module needs Core Foundation and FSEvents on macOS (only when not cross-compiling) // These frameworks provide the FSEvents API for proper event-driven file system monitoring on macOS. - if (module_type == .watch and target.result.os.tag == .macos and target.query.isNative()) { + if (module_type == .watch and target.result.os.tag == .macos and targetMatchesHost(target)) { test_step.linkFramework("CoreFoundation"); test_step.linkFramework("CoreServices"); } @@ -285,7 +580,7 @@ pub const RocModules = struct { // Add only the necessary dependencies for each module test self.addModuleDependencies(test_step, module_type); - // Link zstd for bundle module + // Link zstd for bundle module (unbundle uses stdlib zstd) if (module_type == .bundle) { if (zstd) |z| { test_step.linkLibrary(z.artifact("zstd")); @@ -300,6 +595,9 @@ pub const RocModules = struct { }; } - return tests; + return .{ + .tests = tests, + .forced_passes = forced_passes, + }; } }; diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc new file mode 100644 index 0000000000..9736a29981 --- /dev/null +++ b/src/build/roc/Builtin.roc @@ -0,0 +1,1116 @@ +Builtin :: [].{ + Str :: [ProvidedByCompiler].{ + Utf8Problem := [ + InvalidStartByte, + UnexpectedEndOfSequence, + ExpectedContinuation, + OverlongEncoding, + CodepointTooLarge, + EncodesSurrogateHalf, + ].{ + is_eq : Utf8Problem, Utf8Problem -> Bool + } + + is_empty : Str -> Bool + concat : Str, Str -> Str + contains : Str, Str -> Bool + trim : Str -> Str + trim_start : Str -> Str + trim_end : Str -> Str + caseless_ascii_equals : Str, Str -> Bool + with_ascii_lowercased : Str -> Str + with_ascii_uppercased : Str -> Str + starts_with : Str, Str -> Bool + ends_with : Str, Str -> Bool + repeat : Str, U64 -> Str + with_prefix : Str, Str -> Str + drop_prefix : Str, Str -> Str + drop_suffix : Str, Str -> Str + count_utf8_bytes : Str -> U64 + with_capacity : U64 -> Str + reserve : Str, U64 -> Str + release_excess_capacity : Str -> Str + to_utf8 : Str -> List(U8) + from_utf8_lossy : List(U8) -> Str + from_utf8 : List(U8) -> Try(Str, [BadUtf8({ problem : Str.Utf8Problem, index : U64 }), ..others]) + split_on : Str, Str -> List(Str) + join_with : List(Str), Str -> Str + + is_eq : Str, Str -> Bool + + inspect : _val -> Str + } + + List(_item) :: [ProvidedByCompiler].{ + len : List(_item) -> U64 + is_empty : List(_item) -> Bool + concat : List(item), List(item) -> List(item) + with_capacity : U64 -> List(item) + sort_with : List(item), (item, item -> [LT, EQ, GT]) -> List(item) + + is_eq : List(item), List(item) -> Bool + where [item.is_eq : item, item -> Bool] + is_eq = |self, other| { + if self.len() != other.len() { + return False + } + + var $index = 0 + + while $index < self.len() { + if list_get_unsafe(self, $index) != list_get_unsafe(other, $index) { + return False + } + + $index = $index + 1 + } + + True + } + + append : List(a), a -> List(a) + + first : List(item) -> Try(item, [ListWasEmpty, ..others]) + first = |list| if List.is_empty(list) { + Try.Err(ListWasEmpty) + } else { + Try.Ok(list_get_unsafe(list, 0)) + } + + get : List(item), U64 -> Try(item, [OutOfBounds, ..others]) + get = |list, index| if index < List.len(list) { + Try.Ok(list_get_unsafe(list, index)) + } else { + Try.Err(OutOfBounds) + } + + for_each! : List(item), (item => {}) => {} + for_each! = |items, cb!| for item in items { + cb!(item) + } + + map : List(a), (a -> b) -> List(b) + map = |list, transform| { + # TODO: Optimize with in-place update when list is unique and element sizes match + var $new_list = List.with_capacity(list.len()) + for item in list { + $new_list = list_append_unsafe($new_list, transform(item)) + } + $new_list + } + + keep_if : List(a), (a -> Bool) -> List(a) + keep_if = |list, predicate| + List.fold( + list, + [], + |acc, elem| + if predicate(elem) { + List.concat(acc, [elem]) + } else { + acc + }, + ) + + drop_if : List(a), (a -> Bool) -> List(a) + drop_if = |list, predicate| + List.fold( + list, + [], + |acc, elem| + if predicate(elem) { + acc + } else { + List.concat(acc, [elem]) + }, + ) + + count_if : List(a), (a -> Bool) -> U64 + count_if = |list, predicate| + List.fold( + list, + 0, + |acc, elem| + if predicate(elem) { + acc + 1 + } else { + acc + }, + ) + + fold : List(item), state, (state, item -> state) -> state + fold = |list, init, step| { + var $state = init + + for item in list { + $state = step($state, item) + } + + $state + } + + fold_rev : List(item), state, (item, state -> state) -> state + fold_rev = |list, init, step| { + var $state = init + var $index = list.len() + + while $index > 0 { + $index = $index - 1 + item = list_get_unsafe(list, $index) + $state = step(item, $state) + } + + $state + } + + any : List(a), (a -> Bool) -> Bool + any = |list, predicate| { + for item in list { + if predicate(item) { + return True + } + } + False + } + + contains : List(a), a -> Bool where [a.is_eq : a, a -> Bool] + contains = |list, elt| { + List.any(list, |x| x == elt) + } + + all : List(a), (a -> Bool) -> Bool + all = |list, predicate| { + for item in list { + if Bool.not(predicate(item)) { + return False + } + } + True + } + + last : List(item) -> Try(item, [ListWasEmpty, ..others]) + last = |list| if List.is_empty(list) { + Try.Err(ListWasEmpty) + } else { + Try.Ok(list_get_unsafe(list, List.len(list) - 1)) + } + + single : item -> List(item) + single = |x| [x] + + drop_at : List(a), U64 -> List(a) + + sublist : List(a), { start : U64, len : U64 } -> List(a) + + take_first : List(a), U64 -> List(a) + take_first = |list, n| { + List.sublist(list, { len: n, start: 0 }) + } + + take_last : List(a), U64 -> List(a) + take_last = |list, n| { + len = List.len(list) + start = if (len <= n) 0 else len - n + List.sublist(list, { start: start, len: len }) + } + + drop_first : List(a), U64 -> List(a) + drop_first = |list, n| { + len = List.len(list) + List.sublist(list, { start: n, len: len }) + } + + drop_last : List(a), U64 -> List(a) + drop_last = |list, n| { + len = List.len(list) + take_len = if (len <= n) 0 else len - n + List.sublist(list, { start: 0, len: take_len }) + } + + join_with : List(item), item -> item + where [item.join_with : List(item), item -> item] + join_with = |list, joiner| { + Item : item + Item.join_with(list, joiner) + } + + repeat : a, U64 -> List(a) + repeat = |item, n| { + var $list = List.with_capacity(n) + var $count = 0 + while $count < n { + $list = List.append($list, item) + $count = $count + 1 + } + $list + } + + } + + Bool := [False, True].{ + not : Bool -> Bool + not = |bool| match bool { + Bool.True => Bool.False + Bool.False => Bool.True + } + + is_eq : Bool, Bool -> Bool + + # encoder : Bool -> Encoder(fmt, []) + # where [fmt implements EncoderFormatting] + # encoder = + + # Encoder fmt := List U8, fmt -> List U8 where fmt implements EncoderFormatting + } + + Box(item) :: [ProvidedByCompiler].{ + box : item -> Box(item) + unbox : Box(item) -> item + } + + Try(ok, err) := [Ok(ok), Err(err)].{ + is_ok : Try(_ok, _err) -> Bool + is_ok = |try| match try { + Ok(_) => True + Err(_) => False + } + + is_err : Try(_ok, _err) -> Bool + is_err = |try| match try { + Ok(_) => False + Err(_) => True + } + + ok_or : Try(ok, _err), ok -> ok + ok_or = |try, fallback| match try { + Ok(val) => val + Err(_) => fallback + } + + err_or : Try(_ok, err), err -> err + err_or = |try, fallback| match try { + Err(val) => val + Ok(_) => fallback + } + + map_ok : Try(a, err), (a -> b) -> Try(b, err) + map_ok = |try, transform| match try { + Err(err) => Err(err) + Ok(a) => Ok(transform(a)) + } + + map_err : Try(ok, a), (a -> b) -> Try(ok, b) + map_err = |try, transform| match try { + Err(a) => Err(transform(a)) + Ok(ok) => Ok(ok) + } + + is_eq : Try(ok, err), Try(ok, err) -> Bool + where [ + ok.is_eq : ok, ok -> Bool, + err.is_eq : err, err -> Bool, + ] + is_eq = |a, b| match a { + Ok(a_val) => { + match b { + Ok(b_val) => a_val.is_eq(b_val) + Err(_) => False + } + } + Err(a_val) => { + match b { + Ok(_) => False + Err(b_val) => a_val.is_eq(b_val) + } + } + } + } + + Dict :: [EmptyDict].{} + + Set(item) :: [].{ + is_eq : Set(item), Set(item) -> Bool + is_eq = |_a, _b| Bool.False + } + + Num :: {}.{ + Numeral :: [ + Self( + { # TODO get rid of the "Self" wrapper once we have nominal records" + # True iff there was a minus sign in front of the literal + is_negative : Bool, + # Base-256 digits before and after the decimal point, with any underscores + # and leading/trailing zeros removed from the source code. + # + # Example: If I write "0356.5170" in the source file, that will be: + # - [1, 100] before the pt, because in base-256, 356 = (1 * 256^1) + (100 * 256^0) + # - [2, 5] after the pt, because in base-256, 517 = (2 * 256^1) + (5 * 256^0) + # + # This design compactly represents the digits without wasting any memory + # (because base-256 stores each digit using every single bit of the U8), and also + # allows arbitrary digit length so that userspace custom number types can work with + # arbitrarily long number literals as long as the number types can support them. + digits_before_pt : List(U8), + digits_after_pt : List(U8), + }, + ), + ].{ + is_negative : Numeral -> Bool + is_negative = |self| match self { + # TODO make this a nominal record once we have those + Self({ is_negative: neg, digits_before_pt: _, digits_after_pt: _ }) => neg + } + } + + U8 :: [].{ + to_str : U8 -> Str + is_zero : U8 -> Bool + is_eq : U8, U8 -> Bool + is_gt : U8, U8 -> Bool + is_gte : U8, U8 -> Bool + is_lt : U8, U8 -> Bool + is_lte : U8, U8 -> Bool + + plus : U8, U8 -> U8 + minus : U8, U8 -> U8 + times : U8, U8 -> U8 + div_by : U8, U8 -> U8 + div_trunc_by : U8, U8 -> U8 + rem_by : U8, U8 -> U8 + mod_by : U8, U8 -> U8 + abs_diff : U8, U8 -> U8 + + shift_left_by : U8, U8 -> U8 + shift_right_by : U8, U8 -> U8 + shift_right_zf_by : U8, U8 -> U8 + + from_int_digits : List(U8) -> Try(U8, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(U8, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(U8, [BadNumStr, ..others]) + + # # List of integers beginning with this `U8` and ending with the other `U8`. + # # (Use [until] instead to end with the other `U8` minus one.) + # # Returns an empty list if this `U8` is greater than the other. + to : U8, U8 -> List(U8) + to = |start, end| range_to(start, end) + + # # List of integers beginning with this `U8` and ending with the other `U8` minus one. + # # (Use [to] instead to end with the other `U8` exactly, instead of minus one.) + # # Returns an empty list if this `U8` is greater than or equal to the other. + until : U8, U8 -> List(U8) + until = |start, end| range_until(start, end) + + # Conversions to signed integers (I8 is lossy, others are safe) + to_i8_wrap : U8 -> I8 + to_i8_try : U8 -> Try(I8, [OutOfRange, ..others]) + to_i16 : U8 -> I16 + to_i32 : U8 -> I32 + to_i64 : U8 -> I64 + to_i128 : U8 -> I128 + + # Conversions to unsigned integers (all safe widening) + to_u16 : U8 -> U16 + to_u32 : U8 -> U32 + to_u64 : U8 -> U64 + to_u128 : U8 -> U128 + + # Conversions to floating point (all safe) + to_f32 : U8 -> F32 + to_f64 : U8 -> F64 + to_dec : U8 -> Dec + } + + I8 :: [].{ + to_str : I8 -> Str + is_zero : I8 -> Bool + is_negative : I8 -> Bool + is_positive : I8 -> Bool + is_eq : I8, I8 -> Bool + is_gt : I8, I8 -> Bool + is_gte : I8, I8 -> Bool + is_lt : I8, I8 -> Bool + is_lte : I8, I8 -> Bool + + negate : I8 -> I8 + abs : I8 -> I8 + plus : I8, I8 -> I8 + minus : I8, I8 -> I8 + times : I8, I8 -> I8 + div_by : I8, I8 -> I8 + div_trunc_by : I8, I8 -> I8 + rem_by : I8, I8 -> I8 + mod_by : I8, I8 -> I8 + abs_diff : I8, I8 -> U8 + + shift_left_by : I8, U8 -> I8 + shift_right_by : I8, U8 -> I8 + shift_right_zf_by : I8, U8 -> I8 + + from_int_digits : List(U8) -> Try(I8, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(I8, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(I8, [BadNumStr, ..others]) + + # Conversions to signed integers (all safe widening) + to_i16 : I8 -> I16 + to_i32 : I8 -> I32 + to_i64 : I8 -> I64 + to_i128 : I8 -> I128 + + # Conversions to unsigned integers (all lossy for negative values) + to_u8_wrap : I8 -> U8 + to_u8_try : I8 -> Try(U8, [OutOfRange, ..others]) + to_u16_wrap : I8 -> U16 + to_u16_try : I8 -> Try(U16, [OutOfRange, ..others]) + to_u32_wrap : I8 -> U32 + to_u32_try : I8 -> Try(U32, [OutOfRange, ..others]) + to_u64_wrap : I8 -> U64 + to_u64_try : I8 -> Try(U64, [OutOfRange, ..others]) + to_u128_wrap : I8 -> U128 + to_u128_try : I8 -> Try(U128, [OutOfRange, ..others]) + + # Conversions to floating point (all safe) + to_f32 : I8 -> F32 + to_f64 : I8 -> F64 + to_dec : I8 -> Dec + } + + U16 :: [].{ + to_str : U16 -> Str + is_zero : U16 -> Bool + is_eq : U16, U16 -> Bool + is_gt : U16, U16 -> Bool + is_gte : U16, U16 -> Bool + is_lt : U16, U16 -> Bool + is_lte : U16, U16 -> Bool + + plus : U16, U16 -> U16 + minus : U16, U16 -> U16 + times : U16, U16 -> U16 + div_by : U16, U16 -> U16 + div_trunc_by : U16, U16 -> U16 + rem_by : U16, U16 -> U16 + mod_by : U16, U16 -> U16 + abs_diff : U16, U16 -> U16 + + shift_left_by : U16, U8 -> U16 + shift_right_by : U16, U8 -> U16 + shift_right_zf_by : U16, U8 -> U16 + + from_int_digits : List(U8) -> Try(U16, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(U16, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(U16, [BadNumStr, ..others]) + + # Conversions to signed integers + to_i8_wrap : U16 -> I8 + to_i8_try : U16 -> Try(I8, [OutOfRange, ..others]) + to_i16_wrap : U16 -> I16 + to_i16_try : U16 -> Try(I16, [OutOfRange, ..others]) + to_i32 : U16 -> I32 + to_i64 : U16 -> I64 + to_i128 : U16 -> I128 + + # Conversions to unsigned integers + to_u8_wrap : U16 -> U8 + to_u8_try : U16 -> Try(U8, [OutOfRange, ..others]) + to_u32 : U16 -> U32 + to_u64 : U16 -> U64 + to_u128 : U16 -> U128 + + # Conversions to floating point (all safe) + to_f32 : U16 -> F32 + to_f64 : U16 -> F64 + to_dec : U16 -> Dec + } + + I16 :: [].{ + to_str : I16 -> Str + is_zero : I16 -> Bool + is_negative : I16 -> Bool + is_positive : I16 -> Bool + is_eq : I16, I16 -> Bool + is_gt : I16, I16 -> Bool + is_gte : I16, I16 -> Bool + is_lt : I16, I16 -> Bool + is_lte : I16, I16 -> Bool + + negate : I16 -> I16 + abs : I16 -> I16 + plus : I16, I16 -> I16 + minus : I16, I16 -> I16 + times : I16, I16 -> I16 + div_by : I16, I16 -> I16 + div_trunc_by : I16, I16 -> I16 + rem_by : I16, I16 -> I16 + mod_by : I16, I16 -> I16 + abs_diff : I16, I16 -> U16 + + shift_left_by : I16, U8 -> I16 + shift_right_by : I16, U8 -> I16 + shift_right_zf_by : I16, U8 -> I16 + + from_int_digits : List(U8) -> Try(I16, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(I16, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(I16, [BadNumStr, ..others]) + + # Conversions to signed integers + to_i8_wrap : I16 -> I8 + to_i8_try : I16 -> Try(I8, [OutOfRange, ..others]) + to_i32 : I16 -> I32 + to_i64 : I16 -> I64 + to_i128 : I16 -> I128 + + # Conversions to unsigned integers (all lossy for negative values) + to_u8_wrap : I16 -> U8 + to_u8_try : I16 -> Try(U8, [OutOfRange, ..others]) + to_u16_wrap : I16 -> U16 + to_u16_try : I16 -> Try(U16, [OutOfRange, ..others]) + to_u32_wrap : I16 -> U32 + to_u32_try : I16 -> Try(U32, [OutOfRange, ..others]) + to_u64_wrap : I16 -> U64 + to_u64_try : I16 -> Try(U64, [OutOfRange, ..others]) + to_u128_wrap : I16 -> U128 + to_u128_try : I16 -> Try(U128, [OutOfRange, ..others]) + + # Conversions to floating point (all safe) + to_f32 : I16 -> F32 + to_f64 : I16 -> F64 + to_dec : I16 -> Dec + } + + U32 :: [].{ + to_str : U32 -> Str + is_zero : U32 -> Bool + is_eq : U32, U32 -> Bool + is_gt : U32, U32 -> Bool + is_gte : U32, U32 -> Bool + is_lt : U32, U32 -> Bool + is_lte : U32, U32 -> Bool + + plus : U32, U32 -> U32 + minus : U32, U32 -> U32 + times : U32, U32 -> U32 + div_by : U32, U32 -> U32 + div_trunc_by : U32, U32 -> U32 + rem_by : U32, U32 -> U32 + mod_by : U32, U32 -> U32 + abs_diff : U32, U32 -> U32 + + shift_left_by : U32, U8 -> U32 + shift_right_by : U32, U8 -> U32 + shift_right_zf_by : U32, U8 -> U32 + + from_int_digits : List(U8) -> Try(U32, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(U32, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(U32, [BadNumStr, ..others]) + + # Conversions to signed integers + to_i8_wrap : U32 -> I8 + to_i8_try : U32 -> Try(I8, [OutOfRange, ..others]) + to_i16_wrap : U32 -> I16 + to_i16_try : U32 -> Try(I16, [OutOfRange, ..others]) + to_i32_wrap : U32 -> I32 + to_i32_try : U32 -> Try(I32, [OutOfRange, ..others]) + to_i64 : U32 -> I64 + to_i128 : U32 -> I128 + + # Conversions to unsigned integers + to_u8_wrap : U32 -> U8 + to_u8_try : U32 -> Try(U8, [OutOfRange, ..others]) + to_u16_wrap : U32 -> U16 + to_u16_try : U32 -> Try(U16, [OutOfRange, ..others]) + to_u64 : U32 -> U64 + to_u128 : U32 -> U128 + + # Conversions to floating point (all safe) + to_f32 : U32 -> F32 + to_f64 : U32 -> F64 + to_dec : U32 -> Dec + } + + I32 :: [].{ + to_str : I32 -> Str + is_zero : I32 -> Bool + is_negative : I32 -> Bool + is_positive : I32 -> Bool + is_eq : I32, I32 -> Bool + is_gt : I32, I32 -> Bool + is_gte : I32, I32 -> Bool + is_lt : I32, I32 -> Bool + is_lte : I32, I32 -> Bool + + negate : I32 -> I32 + abs : I32 -> I32 + plus : I32, I32 -> I32 + minus : I32, I32 -> I32 + times : I32, I32 -> I32 + div_by : I32, I32 -> I32 + div_trunc_by : I32, I32 -> I32 + rem_by : I32, I32 -> I32 + mod_by : I32, I32 -> I32 + abs_diff : I32, I32 -> U32 + + shift_left_by : I32, U8 -> I32 + shift_right_by : I32, U8 -> I32 + shift_right_zf_by : I32, U8 -> I32 + + from_int_digits : List(U8) -> Try(I32, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(I32, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(I32, [BadNumStr, ..others]) + + # Conversions to signed integers + to_i8_wrap : I32 -> I8 + to_i8_try : I32 -> Try(I8, [OutOfRange, ..others]) + to_i16_wrap : I32 -> I16 + to_i16_try : I32 -> Try(I16, [OutOfRange, ..others]) + to_i64 : I32 -> I64 + to_i128 : I32 -> I128 + + # Conversions to unsigned integers (all lossy for negative values) + to_u8_wrap : I32 -> U8 + to_u8_try : I32 -> Try(U8, [OutOfRange, ..others]) + to_u16_wrap : I32 -> U16 + to_u16_try : I32 -> Try(U16, [OutOfRange, ..others]) + to_u32_wrap : I32 -> U32 + to_u32_try : I32 -> Try(U32, [OutOfRange, ..others]) + to_u64_wrap : I32 -> U64 + to_u64_try : I32 -> Try(U64, [OutOfRange, ..others]) + to_u128_wrap : I32 -> U128 + to_u128_try : I32 -> Try(U128, [OutOfRange, ..others]) + + # Conversions to floating point (all safe) + to_f32 : I32 -> F32 + to_f64 : I32 -> F64 + to_dec : I32 -> Dec + } + + U64 :: [].{ + to_str : U64 -> Str + is_zero : U64 -> Bool + is_eq : U64, U64 -> Bool + is_gt : U64, U64 -> Bool + is_gte : U64, U64 -> Bool + is_lt : U64, U64 -> Bool + is_lte : U64, U64 -> Bool + + plus : U64, U64 -> U64 + minus : U64, U64 -> U64 + times : U64, U64 -> U64 + div_by : U64, U64 -> U64 + div_trunc_by : U64, U64 -> U64 + rem_by : U64, U64 -> U64 + mod_by : U64, U64 -> U64 + abs_diff : U64, U64 -> U64 + + shift_left_by : U64, U8 -> U64 + shift_right_by : U64, U8 -> U64 + shift_right_zf_by : U64, U8 -> U64 + + from_int_digits : List(U8) -> Try(U64, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(U64, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(U64, [BadNumStr, ..others]) + + # Conversions to signed integers + to_i8_wrap : U64 -> I8 + to_i8_try : U64 -> Try(I8, [OutOfRange, ..others]) + to_i16_wrap : U64 -> I16 + to_i16_try : U64 -> Try(I16, [OutOfRange, ..others]) + to_i32_wrap : U64 -> I32 + to_i32_try : U64 -> Try(I32, [OutOfRange, ..others]) + to_i64_wrap : U64 -> I64 + to_i64_try : U64 -> Try(I64, [OutOfRange, ..others]) + to_i128 : U64 -> I128 + + # Conversions to unsigned integers + to_u8_wrap : U64 -> U8 + to_u8_try : U64 -> Try(U8, [OutOfRange, ..others]) + to_u16_wrap : U64 -> U16 + to_u16_try : U64 -> Try(U16, [OutOfRange, ..others]) + to_u32_wrap : U64 -> U32 + to_u32_try : U64 -> Try(U32, [OutOfRange, ..others]) + to_u128 : U64 -> U128 + + # Conversions to floating point (all safe) + to_f32 : U64 -> F32 + to_f64 : U64 -> F64 + to_dec : U64 -> Dec + } + + I64 :: [].{ + to_str : I64 -> Str + is_zero : I64 -> Bool + is_negative : I64 -> Bool + is_positive : I64 -> Bool + is_eq : I64, I64 -> Bool + is_gt : I64, I64 -> Bool + is_gte : I64, I64 -> Bool + is_lt : I64, I64 -> Bool + is_lte : I64, I64 -> Bool + + negate : I64 -> I64 + abs : I64 -> I64 + plus : I64, I64 -> I64 + minus : I64, I64 -> I64 + times : I64, I64 -> I64 + div_by : I64, I64 -> I64 + div_trunc_by : I64, I64 -> I64 + rem_by : I64, I64 -> I64 + mod_by : I64, I64 -> I64 + abs_diff : I64, I64 -> U64 + + shift_left_by : I64, U8 -> I64 + shift_right_by : I64, U8 -> I64 + shift_right_zf_by : I64, U8 -> I64 + + from_int_digits : List(U8) -> Try(I64, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(I64, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(I64, [BadNumStr, ..others]) + + # Conversions to signed integers + to_i8_wrap : I64 -> I8 + to_i8_try : I64 -> Try(I8, [OutOfRange, ..others]) + to_i16_wrap : I64 -> I16 + to_i16_try : I64 -> Try(I16, [OutOfRange, ..others]) + to_i32_wrap : I64 -> I32 + to_i32_try : I64 -> Try(I32, [OutOfRange, ..others]) + to_i128 : I64 -> I128 + + # Conversions to unsigned integers (all lossy for negative values) + to_u8_wrap : I64 -> U8 + to_u8_try : I64 -> Try(U8, [OutOfRange, ..others]) + to_u16_wrap : I64 -> U16 + to_u16_try : I64 -> Try(U16, [OutOfRange, ..others]) + to_u32_wrap : I64 -> U32 + to_u32_try : I64 -> Try(U32, [OutOfRange, ..others]) + to_u64_wrap : I64 -> U64 + to_u64_try : I64 -> Try(U64, [OutOfRange, ..others]) + to_u128_wrap : I64 -> U128 + to_u128_try : I64 -> Try(U128, [OutOfRange, ..others]) + + # Conversions to floating point (all safe) + to_f32 : I64 -> F32 + to_f64 : I64 -> F64 + to_dec : I64 -> Dec + } + + U128 :: [].{ + to_str : U128 -> Str + is_zero : U128 -> Bool + is_eq : U128, U128 -> Bool + is_gt : U128, U128 -> Bool + is_gte : U128, U128 -> Bool + is_lt : U128, U128 -> Bool + is_lte : U128, U128 -> Bool + + plus : U128, U128 -> U128 + minus : U128, U128 -> U128 + times : U128, U128 -> U128 + div_by : U128, U128 -> U128 + div_trunc_by : U128, U128 -> U128 + rem_by : U128, U128 -> U128 + mod_by : U128, U128 -> U128 + abs_diff : U128, U128 -> U128 + + shift_left_by : U128, U8 -> U128 + shift_right_by : U128, U8 -> U128 + shift_right_zf_by : U128, U8 -> U128 + + from_int_digits : List(U8) -> Try(U128, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(U128, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(U128, [BadNumStr, ..others]) + + # Conversions to signed integers + to_i8_wrap : U128 -> I8 + to_i8_try : U128 -> Try(I8, [OutOfRange, ..others]) + to_i16_wrap : U128 -> I16 + to_i16_try : U128 -> Try(I16, [OutOfRange, ..others]) + to_i32_wrap : U128 -> I32 + to_i32_try : U128 -> Try(I32, [OutOfRange, ..others]) + to_i64_wrap : U128 -> I64 + to_i64_try : U128 -> Try(I64, [OutOfRange, ..others]) + to_i128_wrap : U128 -> I128 + to_i128_try : U128 -> Try(I128, [OutOfRange, ..others]) + + # Conversions to unsigned integers + to_u8_wrap : U128 -> U8 + to_u8_try : U128 -> Try(U8, [OutOfRange, ..others]) + to_u16_wrap : U128 -> U16 + to_u16_try : U128 -> Try(U16, [OutOfRange, ..others]) + to_u32_wrap : U128 -> U32 + to_u32_try : U128 -> Try(U32, [OutOfRange, ..others]) + to_u64_wrap : U128 -> U64 + to_u64_try : U128 -> Try(U64, [OutOfRange, ..others]) + + # Conversions to floating point (all safe) + to_f32 : U128 -> F32 + to_f64 : U128 -> F64 + + # Conversion to Dec (can overflow) + to_dec_try : U128 -> Try(Dec, [OutOfRange, ..others]) + } + + I128 :: [].{ + to_str : I128 -> Str + is_zero : I128 -> Bool + is_negative : I128 -> Bool + is_positive : I128 -> Bool + is_eq : I128, I128 -> Bool + is_gt : I128, I128 -> Bool + is_gte : I128, I128 -> Bool + is_lt : I128, I128 -> Bool + is_lte : I128, I128 -> Bool + + negate : I128 -> I128 + abs : I128 -> I128 + plus : I128, I128 -> I128 + minus : I128, I128 -> I128 + times : I128, I128 -> I128 + div_by : I128, I128 -> I128 + div_trunc_by : I128, I128 -> I128 + rem_by : I128, I128 -> I128 + mod_by : I128, I128 -> I128 + abs_diff : I128, I128 -> U128 + + shift_left_by : I128, U8 -> I128 + shift_right_by : I128, U8 -> I128 + shift_right_zf_by : I128, U8 -> I128 + + from_int_digits : List(U8) -> Try(I128, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(I128, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(I128, [BadNumStr, ..others]) + + # Conversions to signed integers + to_i8_wrap : I128 -> I8 + to_i8_try : I128 -> Try(I8, [OutOfRange, ..others]) + to_i16_wrap : I128 -> I16 + to_i16_try : I128 -> Try(I16, [OutOfRange, ..others]) + to_i32_wrap : I128 -> I32 + to_i32_try : I128 -> Try(I32, [OutOfRange, ..others]) + to_i64_wrap : I128 -> I64 + to_i64_try : I128 -> Try(I64, [OutOfRange, ..others]) + + # Conversions to unsigned integers (all lossy for negative values) + to_u8_wrap : I128 -> U8 + to_u8_try : I128 -> Try(U8, [OutOfRange, ..others]) + to_u16_wrap : I128 -> U16 + to_u16_try : I128 -> Try(U16, [OutOfRange, ..others]) + to_u32_wrap : I128 -> U32 + to_u32_try : I128 -> Try(U32, [OutOfRange, ..others]) + to_u64_wrap : I128 -> U64 + to_u64_try : I128 -> Try(U64, [OutOfRange, ..others]) + to_u128_wrap : I128 -> U128 + to_u128_try : I128 -> Try(U128, [OutOfRange, ..others]) + + # Conversions to floating point (all safe) + to_f32 : I128 -> F32 + to_f64 : I128 -> F64 + + # Conversion to Dec (can overflow) + to_dec_try : I128 -> Try(Dec, [OutOfRange, ..others]) + } + + Dec :: [].{ + to_str : Dec -> Str + is_zero : Dec -> Bool + is_negative : Dec -> Bool + is_positive : Dec -> Bool + is_eq : Dec, Dec -> Bool + is_gt : Dec, Dec -> Bool + is_gte : Dec, Dec -> Bool + is_lt : Dec, Dec -> Bool + is_lte : Dec, Dec -> Bool + + negate : Dec -> Dec + abs : Dec -> Dec + plus : Dec, Dec -> Dec + minus : Dec, Dec -> Dec + times : Dec, Dec -> Dec + div_by : Dec, Dec -> Dec + div_trunc_by : Dec, Dec -> Dec + rem_by : Dec, Dec -> Dec + abs_diff : Dec, Dec -> Dec + + from_int_digits : List(U8) -> Try(Dec, [OutOfRange, ..others]) + from_dec_digits : (List(U8), List(U8)) -> Try(Dec, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(Dec, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(Dec, [BadNumStr, ..others]) + + # Conversions to signed integers (all lossy - truncates fractional part) + to_i8_wrap : Dec -> I8 + to_i8_try : Dec -> Try(I8, [OutOfRange, ..others]) + to_i16_wrap : Dec -> I16 + to_i16_try : Dec -> Try(I16, [OutOfRange, ..others]) + to_i32_wrap : Dec -> I32 + to_i32_try : Dec -> Try(I32, [OutOfRange, ..others]) + to_i64_wrap : Dec -> I64 + to_i64_try : Dec -> Try(I64, [OutOfRange, ..others]) + to_i128_wrap : Dec -> I128 + to_i128_try : Dec -> Try(I128, [OutOfRange, ..others]) + + # Conversions to unsigned integers (all lossy - truncates fractional part) + to_u8_wrap : Dec -> U8 + to_u8_try : Dec -> Try(U8, [OutOfRange, ..others]) + to_u16_wrap : Dec -> U16 + to_u16_try : Dec -> Try(U16, [OutOfRange, ..others]) + to_u32_wrap : Dec -> U32 + to_u32_try : Dec -> Try(U32, [OutOfRange, ..others]) + to_u64_wrap : Dec -> U64 + to_u64_try : Dec -> Try(U64, [OutOfRange, ..others]) + to_u128_wrap : Dec -> U128 + to_u128_try : Dec -> Try(U128, [OutOfRange, ..others]) + + # Conversions to floating point (lossy - Dec has more precision) + to_f32_wrap : Dec -> F32 + to_f32_try : Dec -> Try(F32, [OutOfRange, ..others]) + to_f64 : Dec -> F64 + } + + F32 :: [].{ + to_str : F32 -> Str + is_zero : F32 -> Bool + is_negative : F32 -> Bool + is_positive : F32 -> Bool + is_gt : F32, F32 -> Bool + is_gte : F32, F32 -> Bool + is_lt : F32, F32 -> Bool + is_lte : F32, F32 -> Bool + + negate : F32 -> F32 + abs : F32 -> F32 + plus : F32, F32 -> F32 + minus : F32, F32 -> F32 + times : F32, F32 -> F32 + div_by : F32, F32 -> F32 + div_trunc_by : F32, F32 -> F32 + rem_by : F32, F32 -> F32 + abs_diff : F32, F32 -> F32 + + from_int_digits : List(U8) -> Try(F32, [OutOfRange, ..others]) + from_dec_digits : (List(U8), List(U8)) -> Try(F32, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(F32, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(F32, [BadNumStr, ..others]) + + # Conversions to signed integers (all lossy - truncation + range check) + to_i8_wrap : F32 -> I8 + to_i8_try : F32 -> Try(I8, [OutOfRange, ..others]) + to_i16_wrap : F32 -> I16 + to_i16_try : F32 -> Try(I16, [OutOfRange, ..others]) + to_i32_wrap : F32 -> I32 + to_i32_try : F32 -> Try(I32, [OutOfRange, ..others]) + to_i64_wrap : F32 -> I64 + to_i64_try : F32 -> Try(I64, [OutOfRange, ..others]) + to_i128_wrap : F32 -> I128 + to_i128_try : F32 -> Try(I128, [OutOfRange, ..others]) + + # Conversions to unsigned integers (all lossy - truncation + range check) + to_u8_wrap : F32 -> U8 + to_u8_try : F32 -> Try(U8, [OutOfRange, ..others]) + to_u16_wrap : F32 -> U16 + to_u16_try : F32 -> Try(U16, [OutOfRange, ..others]) + to_u32_wrap : F32 -> U32 + to_u32_try : F32 -> Try(U32, [OutOfRange, ..others]) + to_u64_wrap : F32 -> U64 + to_u64_try : F32 -> Try(U64, [OutOfRange, ..others]) + to_u128_wrap : F32 -> U128 + to_u128_try : F32 -> Try(U128, [OutOfRange, ..others]) + + # Conversion to F64 (safe widening) + to_f64 : F32 -> F64 + } + + F64 :: [].{ + to_str : F64 -> Str + is_zero : F64 -> Bool + is_negative : F64 -> Bool + is_positive : F64 -> Bool + is_gt : F64, F64 -> Bool + is_gte : F64, F64 -> Bool + is_lt : F64, F64 -> Bool + is_lte : F64, F64 -> Bool + + negate : F64 -> F64 + abs : F64 -> F64 + plus : F64, F64 -> F64 + minus : F64, F64 -> F64 + times : F64, F64 -> F64 + div_by : F64, F64 -> F64 + div_trunc_by : F64, F64 -> F64 + rem_by : F64, F64 -> F64 + abs_diff : F64, F64 -> F64 + + from_int_digits : List(U8) -> Try(F64, [OutOfRange, ..others]) + from_dec_digits : (List(U8), List(U8)) -> Try(F64, [OutOfRange, ..others]) + from_numeral : Numeral -> Try(F64, [InvalidNumeral(Str), ..others]) + from_str : Str -> Try(F64, [BadNumStr, ..others]) + + # Conversions to signed integers (all lossy - truncation + range check) + to_i8_wrap : F64 -> I8 + to_i8_try : F64 -> Try(I8, [OutOfRange, ..others]) + to_i16_wrap : F64 -> I16 + to_i16_try : F64 -> Try(I16, [OutOfRange, ..others]) + to_i32_wrap : F64 -> I32 + to_i32_try : F64 -> Try(I32, [OutOfRange, ..others]) + to_i64_wrap : F64 -> I64 + to_i64_try : F64 -> Try(I64, [OutOfRange, ..others]) + to_i128_wrap : F64 -> I128 + to_i128_try : F64 -> Try(I128, [OutOfRange, ..others]) + + # Conversions to unsigned integers (all lossy - truncation + range check) + to_u8_wrap : F64 -> U8 + to_u8_try : F64 -> Try(U8, [OutOfRange, ..others]) + to_u16_wrap : F64 -> U16 + to_u16_try : F64 -> Try(U16, [OutOfRange, ..others]) + to_u32_wrap : F64 -> U32 + to_u32_try : F64 -> Try(U32, [OutOfRange, ..others]) + to_u64_wrap : F64 -> U64 + to_u64_try : F64 -> Try(U64, [OutOfRange, ..others]) + to_u128_wrap : F64 -> U128 + to_u128_try : F64 -> Try(U128, [OutOfRange, ..others]) + + # Conversion to F32 (lossy narrowing) + to_f32_wrap : F64 -> F32 + + to_f32_try : F64 -> Try(F32, [OutOfRange, ..others]) + to_f32_try = |num| { + answer = f64_to_f32_try_unsafe(num) + if answer.success != 0 { + Ok(answer.val_or_memory_garbage) + } else { + Err(OutOfRange) + } + } + } + } +} + +range_to = |var $current, end| { + var $answer = [] # Not bothering with List.with_capacity because this will become an iterator once those exist. + + while $current <= end { + $answer = $answer.append($current) + $current = $current + 1 + } + + $answer +} + +range_until = |var $current, end| { + var $answer = [] # Not bothering with List.with_capacity because this will become an iterator once those exist. + + while $current < end { + $answer = $answer.append($current) + $current = $current + 1 + } + + $answer +} + +# Implemented by the compiler, does not perform bounds checks +list_get_unsafe : List(item), U64 -> item + +# Implemented by the compiler, does not perform bounds checks +list_append_unsafe : List(item), item -> List(item) + +# Unsafe conversion functions - these return simple records instead of Try types +# They are low-level operations that get replaced by the compiler +# Note: success is U8 (0 = false, 1 = true) since Bool is not available at top level +f64_to_f32_try_unsafe : F64 -> { success : U8, val_or_memory_garbage : F32 } diff --git a/src/build/tracy.zig b/src/build/tracy.zig index 4e06ce5496..291a111bb7 100644 --- a/src/build/tracy.zig +++ b/src/build/tracy.zig @@ -47,29 +47,15 @@ const ___tracy_c_zone_context = extern struct { /// The tracy context object for tracking zones. /// Make sure to defer calling end. pub const Ctx = if (enable) ___tracy_c_zone_context else struct { - pub inline fn end(self: @This()) void { - _ = self; - } + pub inline fn end(_: @This()) void {} - pub inline fn addText(self: @This(), text: []const u8) void { - _ = self; - _ = text; - } + pub inline fn addText(_: @This(), _: []const u8) void {} - pub inline fn setName(self: @This(), name: []const u8) void { - _ = self; - _ = name; - } + pub inline fn setName(_: @This(), _: []const u8) void {} - pub inline fn setColor(self: @This(), color: u32) void { - _ = self; - _ = color; - } + pub inline fn setColor(_: @This(), _: u32) void {} - pub inline fn setValue(self: @This(), value: u64) void { - _ = self; - _ = value; - } + pub inline fn setValue(_: @This(), _: u64) void {} }; /// Creates a source location based tracing zone. @@ -147,6 +133,9 @@ pub fn TracyAllocator(comptime name: ?[:0]const u8) type { } fn allocFn(ptr: *anyopaque, len: usize, ptr_align: std.mem.Alignment, ret_addr: usize) ?[*]u8 { + const zone = traceNamed(@src(), "alloc"); + defer zone.end(); + const self: *Self = @ptrCast(@alignCast(ptr)); const result = self.parent_allocator.rawAlloc(len, ptr_align, ret_addr); if (result) |data| { @@ -164,6 +153,9 @@ pub fn TracyAllocator(comptime name: ?[:0]const u8) type { } fn resizeFn(ptr: *anyopaque, buf: []u8, buf_align: std.mem.Alignment, new_len: usize, ret_addr: usize) bool { + const zone = traceNamed(@src(), "resize"); + defer zone.end(); + const self: *Self = @ptrCast(@alignCast(ptr)); if (self.parent_allocator.rawResize(buf, buf_align, new_len, ret_addr)) { if (name) |n| { @@ -183,6 +175,9 @@ pub fn TracyAllocator(comptime name: ?[:0]const u8) type { } fn remapFn(ptr: *anyopaque, buf: []u8, buf_align: std.mem.Alignment, new_len: usize, ret_addr: usize) ?[*]u8 { + const zone = traceNamed(@src(), "remap"); + defer zone.end(); + const self: *Self = @ptrCast(@alignCast(ptr)); if (self.parent_allocator.rawRemap(buf, buf_align, new_len, ret_addr)) |remapped| { if (name) |n| { @@ -202,6 +197,9 @@ pub fn TracyAllocator(comptime name: ?[:0]const u8) type { } fn freeFn(ptr: *anyopaque, buf: []u8, buf_align: std.mem.Alignment, ret_addr: usize) void { + const zone = traceNamed(@src(), "free"); + defer zone.end(); + const self: *Self = @ptrCast(@alignCast(ptr)); self.parent_allocator.rawFree(buf, buf_align, ret_addr); // this condition is to handle free being called on an empty slice that was never even allocated @@ -284,7 +282,9 @@ inline fn frameMarkEnd(comptime name: [:0]const u8) void { extern fn ___tracy_emit_frame_mark_start(name: [*:0]const u8) void; extern fn ___tracy_emit_frame_mark_end(name: [*:0]const u8) void; -inline fn alloc(ptr: [*]u8, len: usize) void { +/// Records a memory allocation with Tracy's memory profiler. +/// Call this after allocating memory to track it in Tracy's memory view. +pub inline fn alloc(ptr: [*]u8, len: usize) void { if (!enable) return; if (enable_callstack) { @@ -304,7 +304,9 @@ inline fn allocNamed(ptr: [*]u8, len: usize, comptime name: [:0]const u8) void { } } -inline fn free(ptr: [*]u8) void { +/// Records a memory deallocation with Tracy's memory profiler. +/// Call this before freeing memory to track it in Tracy's memory view. +pub inline fn free(ptr: [*]u8) void { if (!enable) return; if (enable_callstack) { @@ -404,7 +406,10 @@ extern fn ___tracy_wait_shutdown() void; pub fn waitForShutdown() !void { if (!enable) return; - try std.io.getStdErr().writeAll("Program ended, waiting for tracy to finish collecting data.\n"); + // stderr not available on freestanding + if (comptime builtin.os.tag != .freestanding) { + try std.fs.File.stderr().writeAll("Program ended, waiting for tracy to finish collecting data.\n"); + } ___tracy_wait_shutdown(); } diff --git a/src/build/zig_llvm.cpp b/src/build/zig_llvm.cpp index a21e53cdf7..cc78ad037d 100644 --- a/src/build/zig_llvm.cpp +++ b/src/build/zig_llvm.cpp @@ -50,10 +50,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -80,8 +82,8 @@ static const bool assertions_on = false; LLVMTargetMachineRef ZigLLVMCreateTargetMachine(LLVMTargetRef T, const char *Triple, const char *CPU, const char *Features, LLVMCodeGenOptLevel Level, LLVMRelocMode Reloc, - LLVMCodeModel CodeModel, bool function_sections, bool data_sections, ZigLLVMABIType float_abi, - const char *abi_name) + LLVMCodeModel CodeModel, bool function_sections, bool data_sections, ZigLLVMFloatABI float_abi, + const char *abi_name, bool emulated_tls) { std::optional RM; switch (Reloc){ @@ -128,16 +130,17 @@ LLVMTargetMachineRef ZigLLVMCreateTargetMachine(LLVMTargetRef T, const char *Tri TargetOptions opt; + opt.UseInitArray = true; opt.FunctionSections = function_sections; opt.DataSections = data_sections; switch (float_abi) { - case ZigLLVMABITypeDefault: + case ZigLLVMFloatABI_Default: opt.FloatABIType = FloatABI::Default; break; - case ZigLLVMABITypeSoft: + case ZigLLVMFloatABI_Soft: opt.FloatABIType = FloatABI::Soft; break; - case ZigLLVMABITypeHard: + case ZigLLVMFloatABI_Hard: opt.FloatABIType = FloatABI::Hard; break; } @@ -146,6 +149,10 @@ LLVMTargetMachineRef ZigLLVMCreateTargetMachine(LLVMTargetRef T, const char *Tri opt.MCOptions.ABIName = abi_name; } + if (emulated_tls) { + opt.EmulatedTLS = true; + } + TargetMachine *TM = reinterpret_cast(T)->createTargetMachine(Triple, CPU, Features, opt, RM, CM, OL, JIT); return reinterpret_cast(TM); @@ -188,37 +195,56 @@ struct TimeTracerRAII { }; } // end anonymous namespace -bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMModuleRef module_ref, - char **error_message, bool is_debug, - bool is_small, bool time_report, bool tsan, bool lto, - const char *asm_filename, const char *bin_filename, - const char *llvm_ir_filename, const char *bitcode_filename) +static SanitizerCoverageOptions getSanCovOptions(ZigLLVMCoverageOptions z) { + SanitizerCoverageOptions o; + o.CoverageType = (SanitizerCoverageOptions::Type)z.CoverageType; + o.IndirectCalls = z.IndirectCalls; + o.TraceBB = z.TraceBB; + o.TraceCmp = z.TraceCmp; + o.TraceDiv = z.TraceDiv; + o.TraceGep = z.TraceGep; + o.Use8bitCounters = z.Use8bitCounters; + o.TracePC = z.TracePC; + o.TracePCGuard = z.TracePCGuard; + o.Inline8bitCounters = z.Inline8bitCounters; + o.InlineBoolFlag = z.InlineBoolFlag; + o.PCTable = z.PCTable; + o.NoPrune = z.NoPrune; + o.StackDepth = z.StackDepth; + o.TraceLoads = z.TraceLoads; + o.TraceStores = z.TraceStores; + o.CollectControlFlow = z.CollectControlFlow; + return o; +} + +ZIG_EXTERN_C bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMModuleRef module_ref, + char **error_message, const ZigLLVMEmitOptions *options) { - TimePassesIsEnabled = time_report; + TimePassesIsEnabled = options->time_report_out != nullptr; raw_fd_ostream *dest_asm_ptr = nullptr; raw_fd_ostream *dest_bin_ptr = nullptr; raw_fd_ostream *dest_bitcode_ptr = nullptr; - if (asm_filename) { + if (options->asm_filename) { std::error_code EC; - dest_asm_ptr = new(std::nothrow) raw_fd_ostream(asm_filename, EC, sys::fs::OF_None); + dest_asm_ptr = new(std::nothrow) raw_fd_ostream(options->asm_filename, EC, sys::fs::OF_None); if (EC) { *error_message = strdup((const char *)StringRef(EC.message()).bytes_begin()); return true; } } - if (bin_filename) { + if (options->bin_filename) { std::error_code EC; - dest_bin_ptr = new(std::nothrow) raw_fd_ostream(bin_filename, EC, sys::fs::OF_None); + dest_bin_ptr = new(std::nothrow) raw_fd_ostream(options->bin_filename, EC, sys::fs::OF_None); if (EC) { *error_message = strdup((const char *)StringRef(EC.message()).bytes_begin()); return true; } } - if (bitcode_filename) { + if (options->bitcode_filename) { std::error_code EC; - dest_bitcode_ptr = new(std::nothrow) raw_fd_ostream(bitcode_filename, EC, sys::fs::OF_None); + dest_bitcode_ptr = new(std::nothrow) raw_fd_ostream(options->bitcode_filename, EC, sys::fs::OF_None); if (EC) { *error_message = strdup((const char *)StringRef(EC.message()).bytes_begin()); return true; @@ -234,20 +260,29 @@ bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMM std::string ProcName = "zig-"; ProcName += std::to_string(PID); TimeTracerRAII TimeTracer(ProcName, - bin_filename? bin_filename : asm_filename); + options->bin_filename? options->bin_filename : options->asm_filename); TargetMachine &target_machine = *reinterpret_cast(targ_machine_ref); - target_machine.setO0WantsFastISel(true); + + if (options->allow_fast_isel) { + target_machine.setO0WantsFastISel(true); + } else { + target_machine.setFastISel(false); + } + + if (!options->allow_machine_outliner) { + target_machine.setMachineOutliner(false); + } Module &llvm_module = *unwrap(module_ref); // Pipeline configurations PipelineTuningOptions pipeline_opts; - pipeline_opts.LoopUnrolling = !is_debug; - pipeline_opts.SLPVectorization = !is_debug; - pipeline_opts.LoopVectorization = !is_debug; - pipeline_opts.LoopInterleaving = !is_debug; - pipeline_opts.MergeFunctions = !is_debug; + pipeline_opts.LoopUnrolling = !options->is_debug; + pipeline_opts.SLPVectorization = !options->is_debug; + pipeline_opts.LoopVectorization = !options->is_debug; + pipeline_opts.LoopInterleaving = !options->is_debug; + pipeline_opts.MergeFunctions = !options->is_debug; // Instrumentations PassInstrumentationCallbacks instr_callbacks; @@ -277,54 +312,70 @@ bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMM pass_builder.registerCGSCCAnalyses(cgscc_am); pass_builder.registerFunctionAnalyses(function_am); pass_builder.registerLoopAnalyses(loop_am); - pass_builder.crossRegisterProxies(loop_am, function_am, - cgscc_am, module_am); + pass_builder.crossRegisterProxies(loop_am, function_am, cgscc_am, module_am); - // IR verification - if (assertions_on) { - // Verify the input - pass_builder.registerPipelineStartEPCallback( - [](ModulePassManager &module_pm, OptimizationLevel OL) { - module_pm.addPass(VerifierPass()); - }); - // Verify the output - pass_builder.registerOptimizerLastEPCallback( - [](ModulePassManager &module_pm, OptimizationLevel OL) { - module_pm.addPass(VerifierPass()); - }); - } + pass_builder.registerPipelineStartEPCallback([&](ModulePassManager &module_pm, OptimizationLevel level) { + // Verify the input + if (assertions_on) { + module_pm.addPass(VerifierPass()); + } - // Passes specific for release build - if (!is_debug) { - pass_builder.registerPipelineStartEPCallback( - [](ModulePassManager &module_pm, OptimizationLevel OL) { - module_pm.addPass( - createModuleToFunctionPassAdaptor(AddDiscriminatorsPass())); - }); - } + if (!options->is_debug) { + module_pm.addPass(createModuleToFunctionPassAdaptor(AddDiscriminatorsPass())); + } + }); - // Thread sanitizer - if (tsan) { - pass_builder.registerOptimizerLastEPCallback([](ModulePassManager &module_pm, OptimizationLevel level) { - module_pm.addPass(ModuleThreadSanitizerPass()); - module_pm.addPass(createModuleToFunctionPassAdaptor(ThreadSanitizerPass())); - }); - } + const bool early_san = options->is_debug; + + pass_builder.registerOptimizerEarlyEPCallback([&](ModulePassManager &module_pm, OptimizationLevel level, ThinOrFullLTOPhase lto_phase) { + if (early_san) { + // Code coverage instrumentation. + if (options->sancov) { + module_pm.addPass(SanitizerCoveragePass(getSanCovOptions(options->coverage))); + } + + // Thread sanitizer + if (options->tsan) { + module_pm.addPass(ModuleThreadSanitizerPass()); + module_pm.addPass(createModuleToFunctionPassAdaptor(ThreadSanitizerPass())); + } + } + }); + + pass_builder.registerOptimizerLastEPCallback([&](ModulePassManager &module_pm, OptimizationLevel level, ThinOrFullLTOPhase lto_phase) { + if (!early_san) { + // Code coverage instrumentation. + if (options->sancov) { + module_pm.addPass(SanitizerCoveragePass(getSanCovOptions(options->coverage))); + } + + // Thread sanitizer + if (options->tsan) { + module_pm.addPass(ModuleThreadSanitizerPass()); + module_pm.addPass(createModuleToFunctionPassAdaptor(ThreadSanitizerPass())); + } + } + + // Verify the output + if (assertions_on) { + module_pm.addPass(VerifierPass()); + } + }); ModulePassManager module_pm; OptimizationLevel opt_level; // Setting up the optimization level - if (is_debug) + if (options->is_debug) opt_level = OptimizationLevel::O0; - else if (is_small) + else if (options->is_small) opt_level = OptimizationLevel::Oz; else opt_level = OptimizationLevel::O3; // Initialize the PassManager if (opt_level == OptimizationLevel::O0) { - module_pm = pass_builder.buildO0DefaultPipeline(opt_level, lto); - } else if (lto) { + module_pm = pass_builder.buildO0DefaultPipeline(opt_level, static_cast(options->lto)); + } else if (options->lto) { module_pm = pass_builder.buildLTOPreLinkDefaultPipeline(opt_level); } else { module_pm = pass_builder.buildPerModuleDefaultPipeline(opt_level); @@ -335,7 +386,7 @@ bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMM codegen_pm.add( createTargetTransformInfoWrapperPass(target_machine.getTargetIRAnalysis())); - if (dest_bin && !lto) { + if (dest_bin && !options->lto) { if (target_machine.addPassesToEmitFile(codegen_pm, *dest_bin, nullptr, CodeGenFileType::ObjectFile)) { *error_message = strdup("TargetMachine can't emit an object file"); return true; @@ -354,23 +405,30 @@ bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMM // Code generation phase codegen_pm.run(llvm_module); - if (llvm_ir_filename) { - if (LLVMPrintModuleToFile(module_ref, llvm_ir_filename, error_message)) { + if (options->llvm_ir_filename) { + if (LLVMPrintModuleToFile(module_ref, options->llvm_ir_filename, error_message)) { return true; } } - if (dest_bin && lto) { + if (dest_bin && options->lto) { WriteBitcodeToFile(llvm_module, *dest_bin); } if (dest_bitcode) { WriteBitcodeToFile(llvm_module, *dest_bitcode); } - if (time_report) { - TimerGroup::printAll(errs()); + // This must only happen once we know we've succeeded and will be returning `false`, because + // this code `malloc`s memory which will become owned by the caller (in Zig code). + if (options->time_report_out != nullptr) { + std::string out_str; + auto os = raw_string_ostream(out_str); + TimerGroup::printAll(os); + TimerGroup::clearAll(); + auto c_str = (char *)malloc(out_str.length() + 1); + strcpy(c_str, out_str.c_str()); + *options->time_report_out = c_str; } - return false; } @@ -381,7 +439,7 @@ void ZigLLVMSetOptBisectLimit(LLVMContextRef context_ref, int limit) { } struct ZigDiagnosticHandler : public DiagnosticHandler { - bool BrokenDebugInfo; +bool BrokenDebugInfo; ZigDiagnosticHandler() : BrokenDebugInfo(false) {} bool handleDiagnostics(const DiagnosticInfo &DI) override { // This dyn_cast should be casting to DiagnosticInfoIgnoringInvalidDebugMetadata @@ -393,7 +451,7 @@ struct ZigDiagnosticHandler : public DiagnosticHandler { BrokenDebugInfo = true; } return false; - } + } }; void ZigLLVMEnableBrokenDebugInfoCheck(LLVMContextRef context_ref) { @@ -409,49 +467,19 @@ void ZigLLVMParseCommandLineOptions(size_t argc, const char *const *argv) { cl::ParseCommandLineOptions(argc, argv); } -void ZigLLVMSetModulePICLevel(LLVMModuleRef module) { - unwrap(module)->setPICLevel(PICLevel::Level::BigPIC); +// Initialize all LLVM targets for compilation +void ZigLLVMInitializeAllTargets() { + LLVMInitializeAllTargetInfos(); + LLVMInitializeAllTargets(); + LLVMInitializeAllTargetMCs(); + LLVMInitializeAllAsmParsers(); + LLVMInitializeAllAsmPrinters(); } -void ZigLLVMSetModulePIELevel(LLVMModuleRef module) { - unwrap(module)->setPIELevel(PIELevel::Level::Large); -} - -void ZigLLVMSetModuleCodeModel(LLVMModuleRef module, LLVMCodeModel code_model) { - bool JIT; - unwrap(module)->setCodeModel(*unwrap(code_model, JIT)); - assert(!JIT); -} - -bool ZigLLVMWriteImportLibrary(const char *def_path, const ZigLLVM_ArchType arch, - const char *output_lib_path, bool kill_at) +bool ZigLLVMWriteImportLibrary(const char *def_path, unsigned int coff_machine, + const char *output_lib_path, bool kill_at) { - COFF::MachineTypes machine = COFF::IMAGE_FILE_MACHINE_UNKNOWN; - - switch (arch) { - case ZigLLVM_x86: - machine = COFF::IMAGE_FILE_MACHINE_I386; - break; - case ZigLLVM_x86_64: - machine = COFF::IMAGE_FILE_MACHINE_AMD64; - break; - case ZigLLVM_arm: - case ZigLLVM_armeb: - case ZigLLVM_thumb: - case ZigLLVM_thumbeb: - machine = COFF::IMAGE_FILE_MACHINE_ARMNT; - break; - case ZigLLVM_aarch64: - case ZigLLVM_aarch64_be: - machine = COFF::IMAGE_FILE_MACHINE_ARM64; - break; - default: - break; - } - - if (machine == COFF::IMAGE_FILE_MACHINE_UNKNOWN) { - return true; - } + COFF::MachineTypes machine = static_cast(coff_machine); auto bufOrErr = MemoryBuffer::getFile(def_path); if (!bufOrErr) { @@ -466,7 +494,7 @@ bool ZigLLVMWriteImportLibrary(const char *def_path, const ZigLLVM_ArchType arch return true; } - // The exports-juggling code below is ripped from LLVM's DllToolDriver.cpp + // The exports-juggling code below is ripped from LLVM's DlltoolDriver.cpp // If ExtName is set (if the "ExtName = Name" syntax was used), overwrite // Name with ExtName and clear ExtName. When only creating an import @@ -480,20 +508,22 @@ bool ZigLLVMWriteImportLibrary(const char *def_path, const ZigLLVM_ArchType arch } } - if (machine == COFF::IMAGE_FILE_MACHINE_I386 && kill_at) { + if (kill_at) { for (object::COFFShortExport& E : def->Exports) { - if (!E.AliasTarget.empty() || (!E.Name.empty() && E.Name[0] == '?')) + if (!E.ImportName.empty() || (!E.Name.empty() && E.Name[0] == '?')) continue; - E.SymbolName = E.Name; + if (machine == COFF::IMAGE_FILE_MACHINE_I386) { + // By making sure E.SymbolName != E.Name for decorated symbols, + // writeImportLibrary writes these symbols with the type + // IMPORT_NAME_UNDECORATE. + E.SymbolName = E.Name; + } // Trim off the trailing decoration. Symbols will always have a // starting prefix here (either _ for cdecl/stdcall, @ for fastcall // or ? for C++ functions). Vectorcall functions won't have any // fixed prefix, but the function base name will still be at least // one char. E.Name = E.Name.substr(0, E.Name.find('@', 1)); - // By making sure E.SymbolName != E.Name for decorated symbols, - // writeImportLibrary writes these symbols with the type - // IMPORT_NAME_UNDECORATE. } } @@ -503,30 +533,8 @@ bool ZigLLVMWriteImportLibrary(const char *def_path, const ZigLLVM_ArchType arch } bool ZigLLVMWriteArchive(const char *archive_name, const char **file_names, size_t file_name_count, - ZigLLVM_OSType os_type) + ZigLLVMArchiveKind archive_kind) { - object::Archive::Kind kind; - switch (os_type) { - case ZigLLVM_Win32: - // For some reason llvm-lib passes K_GNU on windows. - // See lib/ToolDrivers/llvm-lib/LibDriver.cpp:168 in libDriverMain - kind = object::Archive::K_GNU; - break; - case ZigLLVM_Linux: - kind = object::Archive::K_GNU; - break; - case ZigLLVM_MacOSX: - case ZigLLVM_Darwin: - case ZigLLVM_IOS: - kind = object::Archive::K_DARWIN; - break; - case ZigLLVM_OpenBSD: - case ZigLLVM_FreeBSD: - kind = object::Archive::K_BSD; - break; - default: - kind = object::Archive::K_GNU; - } SmallVector new_members; for (size_t i = 0; i < file_name_count; i += 1) { Expected new_member = NewArchiveMember::getFile(file_names[i], true); @@ -535,7 +543,7 @@ bool ZigLLVMWriteArchive(const char *archive_name, const char **file_names, size new_members.push_back(std::move(*new_member)); } Error err = writeArchive(archive_name, new_members, - SymtabWritingMode::NormalSymtab, kind, true, false, nullptr); + SymtabWritingMode::NormalSymtab, static_cast(archive_kind), true, false, nullptr); if (err) return true; return false; @@ -583,233 +591,14 @@ bool ZigLLDLinkWasm(int argc, const char **argv, bool can_exit_early, bool disab return lld::wasm::link(args, llvm::outs(), llvm::errs(), can_exit_early, disable_output); } -static_assert((Triple::ArchType)ZigLLVM_UnknownArch == Triple::UnknownArch, ""); -static_assert((Triple::ArchType)ZigLLVM_arm == Triple::arm, ""); -static_assert((Triple::ArchType)ZigLLVM_armeb == Triple::armeb, ""); -static_assert((Triple::ArchType)ZigLLVM_aarch64 == Triple::aarch64, ""); -static_assert((Triple::ArchType)ZigLLVM_aarch64_be == Triple::aarch64_be, ""); -static_assert((Triple::ArchType)ZigLLVM_aarch64_32 == Triple::aarch64_32, ""); -static_assert((Triple::ArchType)ZigLLVM_arc == Triple::arc, ""); -static_assert((Triple::ArchType)ZigLLVM_avr == Triple::avr, ""); -static_assert((Triple::ArchType)ZigLLVM_bpfel == Triple::bpfel, ""); -static_assert((Triple::ArchType)ZigLLVM_bpfeb == Triple::bpfeb, ""); -static_assert((Triple::ArchType)ZigLLVM_csky == Triple::csky, ""); -static_assert((Triple::ArchType)ZigLLVM_hexagon == Triple::hexagon, ""); -static_assert((Triple::ArchType)ZigLLVM_m68k == Triple::m68k, ""); -static_assert((Triple::ArchType)ZigLLVM_mips == Triple::mips, ""); -static_assert((Triple::ArchType)ZigLLVM_mipsel == Triple::mipsel, ""); -static_assert((Triple::ArchType)ZigLLVM_mips64 == Triple::mips64, ""); -static_assert((Triple::ArchType)ZigLLVM_mips64el == Triple::mips64el, ""); -static_assert((Triple::ArchType)ZigLLVM_msp430 == Triple::msp430, ""); -static_assert((Triple::ArchType)ZigLLVM_ppc == Triple::ppc, ""); -static_assert((Triple::ArchType)ZigLLVM_ppcle == Triple::ppcle, ""); -static_assert((Triple::ArchType)ZigLLVM_ppc64 == Triple::ppc64, ""); -static_assert((Triple::ArchType)ZigLLVM_ppc64le == Triple::ppc64le, ""); -static_assert((Triple::ArchType)ZigLLVM_r600 == Triple::r600, ""); -static_assert((Triple::ArchType)ZigLLVM_amdgcn == Triple::amdgcn, ""); -static_assert((Triple::ArchType)ZigLLVM_riscv32 == Triple::riscv32, ""); -static_assert((Triple::ArchType)ZigLLVM_riscv64 == Triple::riscv64, ""); -static_assert((Triple::ArchType)ZigLLVM_sparc == Triple::sparc, ""); -static_assert((Triple::ArchType)ZigLLVM_sparcv9 == Triple::sparcv9, ""); -static_assert((Triple::ArchType)ZigLLVM_sparcel == Triple::sparcel, ""); -static_assert((Triple::ArchType)ZigLLVM_systemz == Triple::systemz, ""); -static_assert((Triple::ArchType)ZigLLVM_tce == Triple::tce, ""); -static_assert((Triple::ArchType)ZigLLVM_tcele == Triple::tcele, ""); -static_assert((Triple::ArchType)ZigLLVM_thumb == Triple::thumb, ""); -static_assert((Triple::ArchType)ZigLLVM_thumbeb == Triple::thumbeb, ""); -static_assert((Triple::ArchType)ZigLLVM_x86 == Triple::x86, ""); -static_assert((Triple::ArchType)ZigLLVM_x86_64 == Triple::x86_64, ""); -static_assert((Triple::ArchType)ZigLLVM_xcore == Triple::xcore, ""); -static_assert((Triple::ArchType)ZigLLVM_xtensa == Triple::xtensa, ""); -static_assert((Triple::ArchType)ZigLLVM_nvptx == Triple::nvptx, ""); -static_assert((Triple::ArchType)ZigLLVM_nvptx64 == Triple::nvptx64, ""); -static_assert((Triple::ArchType)ZigLLVM_le32 == Triple::le32, ""); -static_assert((Triple::ArchType)ZigLLVM_le64 == Triple::le64, ""); -static_assert((Triple::ArchType)ZigLLVM_amdil == Triple::amdil, ""); -static_assert((Triple::ArchType)ZigLLVM_amdil64 == Triple::amdil64, ""); -static_assert((Triple::ArchType)ZigLLVM_hsail == Triple::hsail, ""); -static_assert((Triple::ArchType)ZigLLVM_hsail64 == Triple::hsail64, ""); -static_assert((Triple::ArchType)ZigLLVM_spir == Triple::spir, ""); -static_assert((Triple::ArchType)ZigLLVM_spir64 == Triple::spir64, ""); -static_assert((Triple::ArchType)ZigLLVM_spirv == Triple::spirv, ""); -static_assert((Triple::ArchType)ZigLLVM_spirv32 == Triple::spirv32, ""); -static_assert((Triple::ArchType)ZigLLVM_spirv64 == Triple::spirv64, ""); -static_assert((Triple::ArchType)ZigLLVM_kalimba == Triple::kalimba, ""); -static_assert((Triple::ArchType)ZigLLVM_shave == Triple::shave, ""); -static_assert((Triple::ArchType)ZigLLVM_lanai == Triple::lanai, ""); -static_assert((Triple::ArchType)ZigLLVM_wasm32 == Triple::wasm32, ""); -static_assert((Triple::ArchType)ZigLLVM_wasm64 == Triple::wasm64, ""); -static_assert((Triple::ArchType)ZigLLVM_renderscript32 == Triple::renderscript32, ""); -static_assert((Triple::ArchType)ZigLLVM_renderscript64 == Triple::renderscript64, ""); -static_assert((Triple::ArchType)ZigLLVM_ve == Triple::ve, ""); -static_assert((Triple::ArchType)ZigLLVM_LastArchType == Triple::LastArchType, ""); +static_assert((FloatABI::ABIType)ZigLLVMFloatABI_Default == FloatABI::ABIType::Default, ""); +static_assert((FloatABI::ABIType)ZigLLVMFloatABI_Soft == FloatABI::ABIType::Soft, ""); +static_assert((FloatABI::ABIType)ZigLLVMFloatABI_Hard == FloatABI::ABIType::Hard, ""); -static_assert((Triple::VendorType)ZigLLVM_UnknownVendor == Triple::UnknownVendor, ""); -static_assert((Triple::VendorType)ZigLLVM_Apple == Triple::Apple, ""); -static_assert((Triple::VendorType)ZigLLVM_PC == Triple::PC, ""); -static_assert((Triple::VendorType)ZigLLVM_SCEI == Triple::SCEI, ""); -static_assert((Triple::VendorType)ZigLLVM_Freescale == Triple::Freescale, ""); -static_assert((Triple::VendorType)ZigLLVM_IBM == Triple::IBM, ""); -static_assert((Triple::VendorType)ZigLLVM_ImaginationTechnologies == Triple::ImaginationTechnologies, ""); -static_assert((Triple::VendorType)ZigLLVM_MipsTechnologies == Triple::MipsTechnologies, ""); -static_assert((Triple::VendorType)ZigLLVM_NVIDIA == Triple::NVIDIA, ""); -static_assert((Triple::VendorType)ZigLLVM_CSR == Triple::CSR, ""); -static_assert((Triple::VendorType)ZigLLVM_AMD == Triple::AMD, ""); -static_assert((Triple::VendorType)ZigLLVM_Mesa == Triple::Mesa, ""); -static_assert((Triple::VendorType)ZigLLVM_SUSE == Triple::SUSE, ""); -static_assert((Triple::VendorType)ZigLLVM_OpenEmbedded == Triple::OpenEmbedded, ""); -static_assert((Triple::VendorType)ZigLLVM_LastVendorType == Triple::LastVendorType, ""); - -static_assert((Triple::OSType)ZigLLVM_UnknownOS == Triple::UnknownOS, ""); -static_assert((Triple::OSType)ZigLLVM_Darwin == Triple::Darwin, ""); -static_assert((Triple::OSType)ZigLLVM_DragonFly == Triple::DragonFly, ""); -static_assert((Triple::OSType)ZigLLVM_FreeBSD == Triple::FreeBSD, ""); -static_assert((Triple::OSType)ZigLLVM_Fuchsia == Triple::Fuchsia, ""); -static_assert((Triple::OSType)ZigLLVM_IOS == Triple::IOS, ""); -// Commented out to work around a Debian/Ubuntu bug. -// See https://github.com/ziglang/zig/issues/2076 -//static_assert((Triple::OSType)ZigLLVM_KFreeBSD == Triple::KFreeBSD, ""); -static_assert((Triple::OSType)ZigLLVM_Linux == Triple::Linux, ""); -static_assert((Triple::OSType)ZigLLVM_Lv2 == Triple::Lv2, ""); -static_assert((Triple::OSType)ZigLLVM_MacOSX == Triple::MacOSX, ""); -static_assert((Triple::OSType)ZigLLVM_NetBSD == Triple::NetBSD, ""); -static_assert((Triple::OSType)ZigLLVM_OpenBSD == Triple::OpenBSD, ""); -static_assert((Triple::OSType)ZigLLVM_Solaris == Triple::Solaris, ""); -static_assert((Triple::OSType)ZigLLVM_UEFI == Triple::UEFI, ""); -static_assert((Triple::OSType)ZigLLVM_Win32 == Triple::Win32, ""); -static_assert((Triple::OSType)ZigLLVM_ZOS == Triple::ZOS, ""); -static_assert((Triple::OSType)ZigLLVM_Haiku == Triple::Haiku, ""); -static_assert((Triple::OSType)ZigLLVM_RTEMS == Triple::RTEMS, ""); -static_assert((Triple::OSType)ZigLLVM_NaCl == Triple::NaCl, ""); -static_assert((Triple::OSType)ZigLLVM_AIX == Triple::AIX, ""); -static_assert((Triple::OSType)ZigLLVM_CUDA == Triple::CUDA, ""); -static_assert((Triple::OSType)ZigLLVM_NVCL == Triple::NVCL, ""); -static_assert((Triple::OSType)ZigLLVM_AMDHSA == Triple::AMDHSA, ""); -static_assert((Triple::OSType)ZigLLVM_PS4 == Triple::PS4, ""); -static_assert((Triple::OSType)ZigLLVM_ELFIAMCU == Triple::ELFIAMCU, ""); -static_assert((Triple::OSType)ZigLLVM_TvOS == Triple::TvOS, ""); -static_assert((Triple::OSType)ZigLLVM_WatchOS == Triple::WatchOS, ""); -static_assert((Triple::OSType)ZigLLVM_DriverKit == Triple::DriverKit, ""); -static_assert((Triple::OSType)ZigLLVM_XROS == Triple::XROS, ""); -static_assert((Triple::OSType)ZigLLVM_Mesa3D == Triple::Mesa3D, ""); -static_assert((Triple::OSType)ZigLLVM_AMDPAL == Triple::AMDPAL, ""); -static_assert((Triple::OSType)ZigLLVM_HermitCore == Triple::HermitCore, ""); -static_assert((Triple::OSType)ZigLLVM_Hurd == Triple::Hurd, ""); -static_assert((Triple::OSType)ZigLLVM_WASI == Triple::WASI, ""); -static_assert((Triple::OSType)ZigLLVM_Emscripten == Triple::Emscripten, ""); -static_assert((Triple::OSType)ZigLLVM_ShaderModel == Triple::ShaderModel, ""); -static_assert((Triple::OSType)ZigLLVM_LiteOS == Triple::LiteOS, ""); -static_assert((Triple::OSType)ZigLLVM_Serenity == Triple::Serenity, ""); -static_assert((Triple::OSType)ZigLLVM_Vulkan == Triple::Vulkan, ""); -static_assert((Triple::OSType)ZigLLVM_LastOSType == Triple::LastOSType, ""); - -static_assert((Triple::EnvironmentType)ZigLLVM_UnknownEnvironment == Triple::UnknownEnvironment, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_GNU == Triple::GNU, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_GNUABIN32 == Triple::GNUABIN32, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_GNUABI64 == Triple::GNUABI64, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_GNUEABI == Triple::GNUEABI, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_GNUEABIHF == Triple::GNUEABIHF, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_GNUF32 == Triple::GNUF32, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_GNUF64 == Triple::GNUF64, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_GNUSF == Triple::GNUSF, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_GNUX32 == Triple::GNUX32, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_GNUILP32 == Triple::GNUILP32, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_CODE16 == Triple::CODE16, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_EABI == Triple::EABI, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_EABIHF == Triple::EABIHF, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Android == Triple::Android, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Musl == Triple::Musl, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_MuslEABI == Triple::MuslEABI, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_MuslEABIHF == Triple::MuslEABIHF, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_MuslX32 == Triple::MuslX32, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_MSVC == Triple::MSVC, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Itanium == Triple::Itanium, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Cygnus == Triple::Cygnus, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_CoreCLR == Triple::CoreCLR, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Simulator == Triple::Simulator, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_MacABI == Triple::MacABI, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Pixel == Triple::Pixel, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Vertex == Triple::Vertex, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Geometry == Triple::Geometry, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Hull == Triple::Hull, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Domain == Triple::Domain, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Compute == Triple::Compute, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Library == Triple::Library, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_RayGeneration == Triple::RayGeneration, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Intersection == Triple::Intersection, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_AnyHit == Triple::AnyHit, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_ClosestHit == Triple::ClosestHit, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Miss == Triple::Miss, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Callable == Triple::Callable, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Mesh == Triple::Mesh, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_Amplification == Triple::Amplification, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_OpenHOS == Triple::OpenHOS, ""); -static_assert((Triple::EnvironmentType)ZigLLVM_LastEnvironmentType == Triple::LastEnvironmentType, ""); - -static_assert((Triple::ObjectFormatType)ZigLLVM_UnknownObjectFormat == Triple::UnknownObjectFormat, ""); -static_assert((Triple::ObjectFormatType)ZigLLVM_COFF == Triple::COFF, ""); -static_assert((Triple::ObjectFormatType)ZigLLVM_ELF == Triple::ELF, ""); -static_assert((Triple::ObjectFormatType)ZigLLVM_GOFF == Triple::GOFF, ""); -static_assert((Triple::ObjectFormatType)ZigLLVM_MachO == Triple::MachO, ""); -static_assert((Triple::ObjectFormatType)ZigLLVM_Wasm == Triple::Wasm, ""); -static_assert((Triple::ObjectFormatType)ZigLLVM_XCOFF == Triple::XCOFF, ""); - -static_assert((CallingConv::ID)ZigLLVM_C == llvm::CallingConv::C, ""); -static_assert((CallingConv::ID)ZigLLVM_Fast == llvm::CallingConv::Fast, ""); -static_assert((CallingConv::ID)ZigLLVM_Cold == llvm::CallingConv::Cold, ""); -static_assert((CallingConv::ID)ZigLLVM_GHC == llvm::CallingConv::GHC, ""); -static_assert((CallingConv::ID)ZigLLVM_HiPE == llvm::CallingConv::HiPE, ""); -static_assert((CallingConv::ID)ZigLLVM_AnyReg == llvm::CallingConv::AnyReg, ""); -static_assert((CallingConv::ID)ZigLLVM_PreserveMost == llvm::CallingConv::PreserveMost, ""); -static_assert((CallingConv::ID)ZigLLVM_PreserveAll == llvm::CallingConv::PreserveAll, ""); -static_assert((CallingConv::ID)ZigLLVM_Swift == llvm::CallingConv::Swift, ""); -static_assert((CallingConv::ID)ZigLLVM_CXX_FAST_TLS == llvm::CallingConv::CXX_FAST_TLS, ""); -static_assert((CallingConv::ID)ZigLLVM_Tail == llvm::CallingConv::Tail, ""); -static_assert((CallingConv::ID)ZigLLVM_CFGuard_Check == llvm::CallingConv::CFGuard_Check, ""); -static_assert((CallingConv::ID)ZigLLVM_SwiftTail == llvm::CallingConv::SwiftTail, ""); -static_assert((CallingConv::ID)ZigLLVM_FirstTargetCC == llvm::CallingConv::FirstTargetCC, ""); -static_assert((CallingConv::ID)ZigLLVM_X86_StdCall == llvm::CallingConv::X86_StdCall, ""); -static_assert((CallingConv::ID)ZigLLVM_X86_FastCall == llvm::CallingConv::X86_FastCall, ""); -static_assert((CallingConv::ID)ZigLLVM_ARM_APCS == llvm::CallingConv::ARM_APCS, ""); -static_assert((CallingConv::ID)ZigLLVM_ARM_AAPCS == llvm::CallingConv::ARM_AAPCS, ""); -static_assert((CallingConv::ID)ZigLLVM_ARM_AAPCS_VFP == llvm::CallingConv::ARM_AAPCS_VFP, ""); -static_assert((CallingConv::ID)ZigLLVM_MSP430_INTR == llvm::CallingConv::MSP430_INTR, ""); -static_assert((CallingConv::ID)ZigLLVM_X86_ThisCall == llvm::CallingConv::X86_ThisCall, ""); -static_assert((CallingConv::ID)ZigLLVM_PTX_Kernel == llvm::CallingConv::PTX_Kernel, ""); -static_assert((CallingConv::ID)ZigLLVM_PTX_Device == llvm::CallingConv::PTX_Device, ""); -static_assert((CallingConv::ID)ZigLLVM_SPIR_FUNC == llvm::CallingConv::SPIR_FUNC, ""); -static_assert((CallingConv::ID)ZigLLVM_SPIR_KERNEL == llvm::CallingConv::SPIR_KERNEL, ""); -static_assert((CallingConv::ID)ZigLLVM_Intel_OCL_BI == llvm::CallingConv::Intel_OCL_BI, ""); -static_assert((CallingConv::ID)ZigLLVM_X86_64_SysV == llvm::CallingConv::X86_64_SysV, ""); -static_assert((CallingConv::ID)ZigLLVM_Win64 == llvm::CallingConv::Win64, ""); -static_assert((CallingConv::ID)ZigLLVM_X86_VectorCall == llvm::CallingConv::X86_VectorCall, ""); -static_assert((CallingConv::ID)ZigLLVM_DUMMY_HHVM == llvm::CallingConv::DUMMY_HHVM, ""); -static_assert((CallingConv::ID)ZigLLVM_DUMMY_HHVM_C == llvm::CallingConv::DUMMY_HHVM_C, ""); -static_assert((CallingConv::ID)ZigLLVM_X86_INTR == llvm::CallingConv::X86_INTR, ""); -static_assert((CallingConv::ID)ZigLLVM_AVR_INTR == llvm::CallingConv::AVR_INTR, ""); -static_assert((CallingConv::ID)ZigLLVM_AVR_SIGNAL == llvm::CallingConv::AVR_SIGNAL, ""); -static_assert((CallingConv::ID)ZigLLVM_AVR_BUILTIN == llvm::CallingConv::AVR_BUILTIN, ""); -static_assert((CallingConv::ID)ZigLLVM_AMDGPU_VS == llvm::CallingConv::AMDGPU_VS, ""); -static_assert((CallingConv::ID)ZigLLVM_AMDGPU_GS == llvm::CallingConv::AMDGPU_GS, ""); -static_assert((CallingConv::ID)ZigLLVM_AMDGPU_PS == llvm::CallingConv::AMDGPU_PS, ""); -static_assert((CallingConv::ID)ZigLLVM_AMDGPU_CS == llvm::CallingConv::AMDGPU_CS, ""); -static_assert((CallingConv::ID)ZigLLVM_AMDGPU_KERNEL == llvm::CallingConv::AMDGPU_KERNEL, ""); -static_assert((CallingConv::ID)ZigLLVM_X86_RegCall == llvm::CallingConv::X86_RegCall, ""); -static_assert((CallingConv::ID)ZigLLVM_AMDGPU_HS == llvm::CallingConv::AMDGPU_HS, ""); -static_assert((CallingConv::ID)ZigLLVM_MSP430_BUILTIN == llvm::CallingConv::MSP430_BUILTIN, ""); -static_assert((CallingConv::ID)ZigLLVM_AMDGPU_LS == llvm::CallingConv::AMDGPU_LS, ""); -static_assert((CallingConv::ID)ZigLLVM_AMDGPU_ES == llvm::CallingConv::AMDGPU_ES, ""); -static_assert((CallingConv::ID)ZigLLVM_AArch64_VectorCall == llvm::CallingConv::AArch64_VectorCall, ""); -static_assert((CallingConv::ID)ZigLLVM_AArch64_SVE_VectorCall == llvm::CallingConv::AArch64_SVE_VectorCall, ""); -static_assert((CallingConv::ID)ZigLLVM_WASM_EmscriptenInvoke == llvm::CallingConv::WASM_EmscriptenInvoke, ""); -static_assert((CallingConv::ID)ZigLLVM_AMDGPU_Gfx == llvm::CallingConv::AMDGPU_Gfx, ""); -static_assert((CallingConv::ID)ZigLLVM_M68k_INTR == llvm::CallingConv::M68k_INTR, ""); -static_assert((CallingConv::ID)ZigLLVM_AArch64_SME_ABI_Support_Routines_PreserveMost_From_X0 == llvm::CallingConv::AArch64_SME_ABI_Support_Routines_PreserveMost_From_X0, ""); -static_assert((CallingConv::ID)ZigLLVM_AArch64_SME_ABI_Support_Routines_PreserveMost_From_X2 == llvm::CallingConv::AArch64_SME_ABI_Support_Routines_PreserveMost_From_X2, ""); -static_assert((CallingConv::ID)ZigLLVM_AMDGPU_CS_Chain == llvm::CallingConv::AMDGPU_CS_Chain, ""); -static_assert((CallingConv::ID)ZigLLVM_AMDGPU_CS_ChainPreserve == llvm::CallingConv::AMDGPU_CS_ChainPreserve, ""); -static_assert((CallingConv::ID)ZigLLVM_M68k_RTD == llvm::CallingConv::M68k_RTD, ""); -static_assert((CallingConv::ID)ZigLLVM_GRAAL == llvm::CallingConv::GRAAL, ""); -static_assert((CallingConv::ID)ZigLLVM_ARM64EC_Thunk_X64 == llvm::CallingConv::ARM64EC_Thunk_X64, ""); -static_assert((CallingConv::ID)ZigLLVM_ARM64EC_Thunk_Native == llvm::CallingConv::ARM64EC_Thunk_Native, ""); -static_assert((CallingConv::ID)ZigLLVM_MaxID == llvm::CallingConv::MaxID, ""); +static_assert((object::Archive::Kind)ZigLLVMArchiveKind_GNU == object::Archive::Kind::K_GNU, ""); +static_assert((object::Archive::Kind)ZigLLVMArchiveKind_GNU64 == object::Archive::Kind::K_GNU64, ""); +static_assert((object::Archive::Kind)ZigLLVMArchiveKind_BSD == object::Archive::Kind::K_BSD, ""); +static_assert((object::Archive::Kind)ZigLLVMArchiveKind_DARWIN == object::Archive::Kind::K_DARWIN, ""); +static_assert((object::Archive::Kind)ZigLLVMArchiveKind_DARWIN64 == object::Archive::Kind::K_DARWIN64, ""); +static_assert((object::Archive::Kind)ZigLLVMArchiveKind_COFF == object::Archive::Kind::K_COFF, ""); +static_assert((object::Archive::Kind)ZigLLVMArchiveKind_AIXBIG == object::Archive::Kind::K_AIXBIG, ""); \ No newline at end of file diff --git a/src/build/zig_llvm.h b/src/build/zig_llvm.h index ded48c31ba..5e754508dd 100644 --- a/src/build/zig_llvm.h +++ b/src/build/zig_llvm.h @@ -24,347 +24,110 @@ // ATTENTION: If you modify this file, be sure to update the corresponding // extern function declarations in the self-hosted compiler. -ZIG_EXTERN_C bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMModuleRef module_ref, - char **error_message, bool is_debug, - bool is_small, bool time_report, bool tsan, bool lto, - const char *asm_filename, const char *bin_filename, - const char *llvm_ir_filename, const char *bitcode_filename); - - -enum ZigLLVMABIType { - ZigLLVMABITypeDefault, // Target-specific (either soft or hard depending on triple, etc). - ZigLLVMABITypeSoft, // Soft float. - ZigLLVMABITypeHard // Hard float. +// synchronize with llvm/include/Transforms/Instrumentation.h::SanitizerCoverageOptions::Type +// synchronize with codegen/llvm/bindings.zig::TargetMachine::EmitOptions::Coverage::Type +enum ZigLLVMCoverageType { + ZigLLVMCoverageType_None = 0, + ZigLLVMCoverageType_Function, + ZigLLVMCoverageType_BB, + ZigLLVMCoverageType_Edge }; +struct ZigLLVMCoverageOptions { + ZigLLVMCoverageType CoverageType; + bool IndirectCalls; + bool TraceBB; + bool TraceCmp; + bool TraceDiv; + bool TraceGep; + bool Use8bitCounters; + bool TracePC; + bool TracePCGuard; + bool Inline8bitCounters; + bool InlineBoolFlag; + bool PCTable; + bool NoPrune; + bool StackDepth; + bool TraceLoads; + bool TraceStores; + bool CollectControlFlow; +}; + +// synchronize with llvm/include/Pass.h::ThinOrFullLTOPhase +// synchronize with codegen/llvm/bindings.zig::EmitOptions::LtoPhase +enum ZigLLVMThinOrFullLTOPhase { + ZigLLVMThinOrFullLTOPhase_None, + ZigLLVMThinOrFullLTOPhase_ThinPreLink, + ZigLLVMThinOrFullLTOPhase_ThinkPostLink, + ZigLLVMThinOrFullLTOPhase_FullPreLink, + ZigLLVMThinOrFullLTOPhase_FullPostLink, +}; + +struct ZigLLVMEmitOptions { + bool is_debug; + bool is_small; + // If not null, and `ZigLLVMTargetMachineEmitToFile` returns `false` indicating success, this + // `char *` will be populated with a `malloc`-allocated string containing the serialized (as + // JSON) time report data. The caller is responsible for freeing that memory. + char **time_report_out; + bool tsan; + bool sancov; + ZigLLVMThinOrFullLTOPhase lto; + bool allow_fast_isel; + bool allow_machine_outliner; + const char *asm_filename; + const char *bin_filename; + const char *llvm_ir_filename; + const char *bitcode_filename; + ZigLLVMCoverageOptions coverage; +}; + +// synchronize with llvm/include/Object/Archive.h::Object::Archive::Kind +// synchronize with codegen/llvm/bindings.zig::ArchiveKind +enum ZigLLVMArchiveKind { + ZigLLVMArchiveKind_GNU, + ZigLLVMArchiveKind_GNU64, + ZigLLVMArchiveKind_BSD, + ZigLLVMArchiveKind_DARWIN, + ZigLLVMArchiveKind_DARWIN64, + ZigLLVMArchiveKind_COFF, + ZigLLVMArchiveKind_AIXBIG, +}; + +// synchronize with llvm/include/Target/TargetOptions.h::FloatABI::ABIType +// synchronize with codegen/llvm/bindings.zig::TargetMachine::FloatABI +enum ZigLLVMFloatABI { + ZigLLVMFloatABI_Default, // Target-specific (either soft or hard depending on triple, etc). + ZigLLVMFloatABI_Soft, // Soft float. + ZigLLVMFloatABI_Hard // Hard float. +}; + +ZIG_EXTERN_C bool ZigLLVMTargetMachineEmitToFile(LLVMTargetMachineRef targ_machine_ref, LLVMModuleRef module_ref, + char **error_message, const ZigLLVMEmitOptions *options); + ZIG_EXTERN_C LLVMTargetMachineRef ZigLLVMCreateTargetMachine(LLVMTargetRef T, const char *Triple, const char *CPU, const char *Features, LLVMCodeGenOptLevel Level, LLVMRelocMode Reloc, - LLVMCodeModel CodeModel, bool function_sections, bool data_sections, enum ZigLLVMABIType float_abi, - const char *abi_name); + LLVMCodeModel CodeModel, bool function_sections, bool data_sections, ZigLLVMFloatABI float_abi, + const char *abi_name, bool emulated_tls); ZIG_EXTERN_C void ZigLLVMSetOptBisectLimit(LLVMContextRef context_ref, int limit); ZIG_EXTERN_C void ZigLLVMEnableBrokenDebugInfoCheck(LLVMContextRef context_ref); ZIG_EXTERN_C bool ZigLLVMGetBrokenDebugInfo(LLVMContextRef context_ref); -enum ZigLLVMTailCallKind { - ZigLLVMTailCallKindNone, - ZigLLVMTailCallKindTail, - ZigLLVMTailCallKindMustTail, - ZigLLVMTailCallKindNoTail, -}; - -enum ZigLLVM_CallingConv { - ZigLLVM_C = 0, - ZigLLVM_Fast = 8, - ZigLLVM_Cold = 9, - ZigLLVM_GHC = 10, - ZigLLVM_HiPE = 11, - ZigLLVM_AnyReg = 13, - ZigLLVM_PreserveMost = 14, - ZigLLVM_PreserveAll = 15, - ZigLLVM_Swift = 16, - ZigLLVM_CXX_FAST_TLS = 17, - ZigLLVM_Tail = 18, - ZigLLVM_CFGuard_Check = 19, - ZigLLVM_SwiftTail = 20, - ZigLLVM_FirstTargetCC = 64, - ZigLLVM_X86_StdCall = 64, - ZigLLVM_X86_FastCall = 65, - ZigLLVM_ARM_APCS = 66, - ZigLLVM_ARM_AAPCS = 67, - ZigLLVM_ARM_AAPCS_VFP = 68, - ZigLLVM_MSP430_INTR = 69, - ZigLLVM_X86_ThisCall = 70, - ZigLLVM_PTX_Kernel = 71, - ZigLLVM_PTX_Device = 72, - ZigLLVM_SPIR_FUNC = 75, - ZigLLVM_SPIR_KERNEL = 76, - ZigLLVM_Intel_OCL_BI = 77, - ZigLLVM_X86_64_SysV = 78, - ZigLLVM_Win64 = 79, - ZigLLVM_X86_VectorCall = 80, - ZigLLVM_DUMMY_HHVM = 81, - ZigLLVM_DUMMY_HHVM_C = 82, - ZigLLVM_X86_INTR = 83, - ZigLLVM_AVR_INTR = 84, - ZigLLVM_AVR_SIGNAL = 85, - ZigLLVM_AVR_BUILTIN = 86, - ZigLLVM_AMDGPU_VS = 87, - ZigLLVM_AMDGPU_GS = 88, - ZigLLVM_AMDGPU_PS = 89, - ZigLLVM_AMDGPU_CS = 90, - ZigLLVM_AMDGPU_KERNEL = 91, - ZigLLVM_X86_RegCall = 92, - ZigLLVM_AMDGPU_HS = 93, - ZigLLVM_MSP430_BUILTIN = 94, - ZigLLVM_AMDGPU_LS = 95, - ZigLLVM_AMDGPU_ES = 96, - ZigLLVM_AArch64_VectorCall = 97, - ZigLLVM_AArch64_SVE_VectorCall = 98, - ZigLLVM_WASM_EmscriptenInvoke = 99, - ZigLLVM_AMDGPU_Gfx = 100, - ZigLLVM_M68k_INTR = 101, - ZigLLVM_AArch64_SME_ABI_Support_Routines_PreserveMost_From_X0 = 102, - ZigLLVM_AArch64_SME_ABI_Support_Routines_PreserveMost_From_X2 = 103, - ZigLLVM_AMDGPU_CS_Chain = 104, - ZigLLVM_AMDGPU_CS_ChainPreserve = 105, - ZigLLVM_M68k_RTD = 106, - ZigLLVM_GRAAL = 107, - ZigLLVM_ARM64EC_Thunk_X64 = 108, - ZigLLVM_ARM64EC_Thunk_Native = 109, - ZigLLVM_MaxID = 1023, -}; - -ZIG_EXTERN_C void ZigLLVMSetModulePICLevel(LLVMModuleRef module); -ZIG_EXTERN_C void ZigLLVMSetModulePIELevel(LLVMModuleRef module); -ZIG_EXTERN_C void ZigLLVMSetModuleCodeModel(LLVMModuleRef module, LLVMCodeModel code_model); +ZIG_EXTERN_C void ZigLLVMInitializeAllTargets(); ZIG_EXTERN_C void ZigLLVMParseCommandLineOptions(size_t argc, const char *const *argv); -// synchronize with llvm/include/ADT/Triple.h::ArchType -// synchronize with std.Target.Cpu.Arch -// synchronize with codegen/llvm/bindings.zig::ArchType -enum ZigLLVM_ArchType { - ZigLLVM_UnknownArch, - - ZigLLVM_arm, // ARM (little endian): arm, armv.*, xscale - ZigLLVM_armeb, // ARM (big endian): armeb - ZigLLVM_aarch64, // AArch64 (little endian): aarch64 - ZigLLVM_aarch64_be, // AArch64 (big endian): aarch64_be - ZigLLVM_aarch64_32, // AArch64 (little endian) ILP32: aarch64_32 - ZigLLVM_arc, // ARC: Synopsys ARC - ZigLLVM_avr, // AVR: Atmel AVR microcontroller - ZigLLVM_bpfel, // eBPF or extended BPF or 64-bit BPF (little endian) - ZigLLVM_bpfeb, // eBPF or extended BPF or 64-bit BPF (big endian) - ZigLLVM_csky, // CSKY: csky - ZigLLVM_dxil, // DXIL 32-bit DirectX bytecode - ZigLLVM_hexagon, // Hexagon: hexagon - ZigLLVM_loongarch32, // LoongArch (32-bit): loongarch32 - ZigLLVM_loongarch64, // LoongArch (64-bit): loongarch64 - ZigLLVM_m68k, // M68k: Motorola 680x0 family - ZigLLVM_mips, // MIPS: mips, mipsallegrex, mipsr6 - ZigLLVM_mipsel, // MIPSEL: mipsel, mipsallegrexe, mipsr6el - ZigLLVM_mips64, // MIPS64: mips64, mips64r6, mipsn32, mipsn32r6 - ZigLLVM_mips64el, // MIPS64EL: mips64el, mips64r6el, mipsn32el, mipsn32r6el - ZigLLVM_msp430, // MSP430: msp430 - ZigLLVM_ppc, // PPC: powerpc - ZigLLVM_ppcle, // PPCLE: powerpc (little endian) - ZigLLVM_ppc64, // PPC64: powerpc64, ppu - ZigLLVM_ppc64le, // PPC64LE: powerpc64le - ZigLLVM_r600, // R600: AMD GPUs HD2XXX - HD6XXX - ZigLLVM_amdgcn, // AMDGCN: AMD GCN GPUs - ZigLLVM_riscv32, // RISC-V (32-bit): riscv32 - ZigLLVM_riscv64, // RISC-V (64-bit): riscv64 - ZigLLVM_sparc, // Sparc: sparc - ZigLLVM_sparcv9, // Sparcv9: Sparcv9 - ZigLLVM_sparcel, // Sparc: (endianness = little). NB: 'Sparcle' is a CPU variant - ZigLLVM_systemz, // SystemZ: s390x - ZigLLVM_tce, // TCE (http://tce.cs.tut.fi/): tce - ZigLLVM_tcele, // TCE little endian (http://tce.cs.tut.fi/): tcele - ZigLLVM_thumb, // Thumb (little endian): thumb, thumbv.* - ZigLLVM_thumbeb, // Thumb (big endian): thumbeb - ZigLLVM_x86, // X86: i[3-9]86 - ZigLLVM_x86_64, // X86-64: amd64, x86_64 - ZigLLVM_xcore, // XCore: xcore - ZigLLVM_xtensa, // Tensilica: Xtensa - ZigLLVM_nvptx, // NVPTX: 32-bit - ZigLLVM_nvptx64, // NVPTX: 64-bit - ZigLLVM_le32, // le32: generic little-endian 32-bit CPU (PNaCl) - ZigLLVM_le64, // le64: generic little-endian 64-bit CPU (PNaCl) - ZigLLVM_amdil, // AMDIL - ZigLLVM_amdil64, // AMDIL with 64-bit pointers - ZigLLVM_hsail, // AMD HSAIL - ZigLLVM_hsail64, // AMD HSAIL with 64-bit pointers - ZigLLVM_spir, // SPIR: standard portable IR for OpenCL 32-bit version - ZigLLVM_spir64, // SPIR: standard portable IR for OpenCL 64-bit version - ZigLLVM_spirv, // SPIR-V with logical memory layout. - ZigLLVM_spirv32, // SPIR-V with 32-bit pointers - ZigLLVM_spirv64, // SPIR-V with 64-bit pointers - ZigLLVM_kalimba, // Kalimba: generic kalimba - ZigLLVM_shave, // SHAVE: Movidius vector VLIW processors - ZigLLVM_lanai, // Lanai: Lanai 32-bit - ZigLLVM_wasm32, // WebAssembly with 32-bit pointers - ZigLLVM_wasm64, // WebAssembly with 64-bit pointers - ZigLLVM_renderscript32, // 32-bit RenderScript - ZigLLVM_renderscript64, // 64-bit RenderScript - ZigLLVM_ve, // NEC SX-Aurora Vector Engine - ZigLLVM_LastArchType = ZigLLVM_ve -}; - -enum ZigLLVM_VendorType { - ZigLLVM_UnknownVendor, - - ZigLLVM_Apple, - ZigLLVM_PC, - ZigLLVM_SCEI, - ZigLLVM_Freescale, - ZigLLVM_IBM, - ZigLLVM_ImaginationTechnologies, - ZigLLVM_MipsTechnologies, - ZigLLVM_NVIDIA, - ZigLLVM_CSR, - ZigLLVM_AMD, - ZigLLVM_Mesa, - ZigLLVM_SUSE, - ZigLLVM_OpenEmbedded, - - ZigLLVM_LastVendorType = ZigLLVM_OpenEmbedded -}; - -// synchronize with llvm/include/ADT/Triple.h::OsType -// synchronize with std.Target.Os.Tag -// synchronize with codegen/llvm/bindings.zig::OsType -enum ZigLLVM_OSType { - ZigLLVM_UnknownOS, - - ZigLLVM_Darwin, - ZigLLVM_DragonFly, - ZigLLVM_FreeBSD, - ZigLLVM_Fuchsia, - ZigLLVM_IOS, - ZigLLVM_KFreeBSD, - ZigLLVM_Linux, - ZigLLVM_Lv2, // PS3 - ZigLLVM_MacOSX, - ZigLLVM_NetBSD, - ZigLLVM_OpenBSD, - ZigLLVM_Solaris, - ZigLLVM_UEFI, - ZigLLVM_Win32, - ZigLLVM_ZOS, - ZigLLVM_Haiku, - ZigLLVM_RTEMS, - ZigLLVM_NaCl, // Native Client - ZigLLVM_AIX, - ZigLLVM_CUDA, // NVIDIA CUDA - ZigLLVM_NVCL, // NVIDIA OpenCL - ZigLLVM_AMDHSA, // AMD HSA Runtime - ZigLLVM_PS4, - ZigLLVM_PS5, - ZigLLVM_ELFIAMCU, - ZigLLVM_TvOS, // Apple tvOS - ZigLLVM_WatchOS, // Apple watchOS - ZigLLVM_DriverKit, // Apple DriverKit - ZigLLVM_XROS, // Apple XROS - ZigLLVM_Mesa3D, - ZigLLVM_AMDPAL, // AMD PAL Runtime - ZigLLVM_HermitCore, // HermitCore Unikernel/Multikernel - ZigLLVM_Hurd, // GNU/Hurd - ZigLLVM_WASI, // Experimental WebAssembly OS - ZigLLVM_Emscripten, - ZigLLVM_ShaderModel, // DirectX ShaderModel - ZigLLVM_LiteOS, - ZigLLVM_Serenity, - ZigLLVM_Vulkan, // Vulkan SPIR-V - ZigLLVM_LastOSType = ZigLLVM_Vulkan -}; - -// Synchronize with target.cpp::abi_list -enum ZigLLVM_EnvironmentType { - ZigLLVM_UnknownEnvironment, - - ZigLLVM_GNU, - ZigLLVM_GNUABIN32, - ZigLLVM_GNUABI64, - ZigLLVM_GNUEABI, - ZigLLVM_GNUEABIHF, - ZigLLVM_GNUF32, - ZigLLVM_GNUF64, - ZigLLVM_GNUSF, - ZigLLVM_GNUX32, - ZigLLVM_GNUILP32, - ZigLLVM_CODE16, - ZigLLVM_EABI, - ZigLLVM_EABIHF, - ZigLLVM_Android, - ZigLLVM_Musl, - ZigLLVM_MuslEABI, - ZigLLVM_MuslEABIHF, - ZigLLVM_MuslX32, - - ZigLLVM_MSVC, - ZigLLVM_Itanium, - ZigLLVM_Cygnus, - ZigLLVM_CoreCLR, - ZigLLVM_Simulator, // Simulator variants of other systems, e.g., Apple's iOS - ZigLLVM_MacABI, // Mac Catalyst variant of Apple's iOS deployment target. - - ZigLLVM_Pixel, - ZigLLVM_Vertex, - ZigLLVM_Geometry, - ZigLLVM_Hull, - ZigLLVM_Domain, - ZigLLVM_Compute, - ZigLLVM_Library, - ZigLLVM_RayGeneration, - ZigLLVM_Intersection, - ZigLLVM_AnyHit, - ZigLLVM_ClosestHit, - ZigLLVM_Miss, - ZigLLVM_Callable, - ZigLLVM_Mesh, - ZigLLVM_Amplification, - ZigLLVM_OpenHOS, - - ZigLLVM_LastEnvironmentType = ZigLLVM_OpenHOS -}; - -enum ZigLLVM_ObjectFormatType { - ZigLLVM_UnknownObjectFormat, - - ZigLLVM_COFF, - ZigLLVM_DXContainer, - ZigLLVM_ELF, - ZigLLVM_GOFF, - ZigLLVM_MachO, - ZigLLVM_SPIRV, - ZigLLVM_Wasm, - ZigLLVM_XCOFF, -}; - -#define ZigLLVM_DIFlags_Zero 0U -#define ZigLLVM_DIFlags_Private 1U -#define ZigLLVM_DIFlags_Protected 2U -#define ZigLLVM_DIFlags_Public 3U -#define ZigLLVM_DIFlags_FwdDecl (1U << 2) -#define ZigLLVM_DIFlags_AppleBlock (1U << 3) -#define ZigLLVM_DIFlags_BlockByrefStruct (1U << 4) -#define ZigLLVM_DIFlags_Virtual (1U << 5) -#define ZigLLVM_DIFlags_Artificial (1U << 6) -#define ZigLLVM_DIFlags_Explicit (1U << 7) -#define ZigLLVM_DIFlags_Prototyped (1U << 8) -#define ZigLLVM_DIFlags_ObjcClassComplete (1U << 9) -#define ZigLLVM_DIFlags_ObjectPointer (1U << 10) -#define ZigLLVM_DIFlags_Vector (1U << 11) -#define ZigLLVM_DIFlags_StaticMember (1U << 12) -#define ZigLLVM_DIFlags_LValueReference (1U << 13) -#define ZigLLVM_DIFlags_RValueReference (1U << 14) -#define ZigLLVM_DIFlags_Reserved (1U << 15) -#define ZigLLVM_DIFlags_SingleInheritance (1U << 16) -#define ZigLLVM_DIFlags_MultipleInheritance (2 << 16) -#define ZigLLVM_DIFlags_VirtualInheritance (3 << 16) -#define ZigLLVM_DIFlags_IntroducedVirtual (1U << 18) -#define ZigLLVM_DIFlags_BitField (1U << 19) -#define ZigLLVM_DIFlags_NoReturn (1U << 20) -#define ZigLLVM_DIFlags_TypePassByValue (1U << 22) -#define ZigLLVM_DIFlags_TypePassByReference (1U << 23) -#define ZigLLVM_DIFlags_EnumClass (1U << 24) -#define ZigLLVM_DIFlags_Thunk (1U << 25) -#define ZigLLVM_DIFlags_NonTrivial (1U << 26) -#define ZigLLVM_DIFlags_BigEndian (1U << 27) -#define ZigLLVM_DIFlags_LittleEndian (1U << 28) -#define ZigLLVM_DIFlags_AllCallsDescribed (1U << 29) - ZIG_EXTERN_C bool ZigLLDLinkCOFF(int argc, const char **argv, bool can_exit_early, bool disable_output); ZIG_EXTERN_C bool ZigLLDLinkELF(int argc, const char **argv, bool can_exit_early, bool disable_output); ZIG_EXTERN_C bool ZigLLDLinkMachO(int argc, const char **argv, bool can_exit_early, bool disable_output); ZIG_EXTERN_C bool ZigLLDLinkWasm(int argc, const char **argv, bool can_exit_early, bool disable_output); ZIG_EXTERN_C bool ZigLLVMWriteArchive(const char *archive_name, const char **file_names, size_t file_name_count, - enum ZigLLVM_OSType os_type); + ZigLLVMArchiveKind archive_kind); -ZIG_EXTERN_C bool ZigLLVMWriteImportLibrary(const char *def_path, const enum ZigLLVM_ArchType arch, - const char *output_lib_path, bool kill_at); +ZIG_EXTERN_C bool ZigLLVMWriteImportLibrary(const char *def_path, unsigned int coff_machine, + const char *output_lib_path, bool kill_at); -#endif +#endif \ No newline at end of file diff --git a/src/builtins/OWNERSHIP.md b/src/builtins/OWNERSHIP.md new file mode 100644 index 0000000000..b6ceb7a5e7 --- /dev/null +++ b/src/builtins/OWNERSHIP.md @@ -0,0 +1,236 @@ +# Ownership Semantics in Roc Builtins + +This document defines the canonical terminology for ownership semantics in Roc's builtin functions. +Understanding these patterns is critical for correctly implementing and calling builtins. + +## Core Invariant + +**refcount = number of live references to the data** + +When refcount reaches 0, memory is freed. + +Basic operations: +1. **Create**: allocate with refcount = 1 +2. **Share**: increment refcount (data is shared, both references valid) +3. **Release**: decrement refcount (if 0, free memory) + +--- + +## Argument Handling (2 patterns) + +These describe how a function treats its input arguments. + +### Borrow + +A **borrowing** function reads its arguments without affecting their refcount. + +- Caller retains ownership +- No refcount change at call boundary +- Caller can still use argument after call + +**Examples**: `strEqual`, `listLen`, `strContains`, `countUtf8Bytes` + +### Consume + +A **consuming** function takes ownership of its argument. + +- Caller transfers ownership to callee +- Caller must not use argument after call (logically moved) +- Function is responsible for cleanup (decref when done) + +**Examples**: `strConcat`, `listConcat`, `strJoinWith` + +--- + +## Result Patterns (3 types) + +These describe the relationship between a function's result and its arguments. + +### Independent + +Result is a new allocation, unrelated to arguments. + +- Normal ownership: caller owns result +- Caller must decref when done + +**Example**: `strConcat` returns newly allocated combined string + +### Copy-on-Write (Same-if-unique) + +Result may be the same allocation as an argument. + +- Consumes the input argument +- If `isUnique()`: mutates in place, returns same pointer +- If shared: decrefs argument internally, allocates new, returns new pointer +- **Critical**: `result.ptr` may equal `arg.ptr` + +**Examples**: `strWithAsciiUppercased`, `strTrim`, `listAppend` + +**Interpreter handling**: Check if `result.bytes == arg.bytes`: +- If same: skip decref (ownership passed through) +- If different: builtin already decrefd internally + +### Seamless Slice + +Result shares underlying data with argument via seamless slice. + +- Borrows argument (caller keeps ownership) +- Builtin calls `incref` internally to share the allocation +- Result points into argument's memory +- `SEAMLESS_SLICE_BIT` marks the slice in length field + +**Examples**: `strToUtf8`, `substringUnsafe`, `listSublist` + +**Interpreter handling**: Decref the argument after call (builtin only incref'd for sharing; +the original binding's reference must still be released) + +--- + +## Complete Taxonomy + +The key insight is that seamless slices can be created by either borrowing OR consuming functions: + +| Pattern | Arg Handling | Result Type | Interpreter After Call | +|---------|--------------|-------------|------------------------| +| Pure borrow | Borrow | Independent | Decref arg | +| Borrowing seamless slice | Borrow | Slice (incref'd) | Decref arg | +| Pure consume | Consume | Independent | **Don't decref** | +| Copy-on-write | Consume | Same-if-unique | **Don't decref** | +| Consuming seamless slice | Consume | Slice (inherited) | **Don't decref** | + +**Simple rule**: Decref if and only if the builtin **borrows**. Never decref for **consume**. + +### Why This Matters + +For **borrowing** seamless slice (e.g., `strToUtf8`): +- Builtin calls `incref` to share the allocation +- Caller still has their reference +- Interpreter must decref (release the borrowed copy) + +For **consuming** seamless slice (e.g., `strTrim` with offset): +- Builtin does NOT call `incref` +- Slice inherits the caller's reference +- Interpreter must NOT decref (ownership transferred) + +### Copy-on-Write Detail + +For copy-on-write builtins like `strWithAsciiUppercased`: +- If input is **unique**: mutates in place, returns same pointer +- If input is **shared**: builtin decrefs internally, allocates new + +In BOTH cases, the interpreter should NOT decref: +- Unique case: ownership passed through to result +- Shared case: builtin already handled the decref + +The previous heuristic (`result.bytes == arg.bytes`) was incomplete—it missed +the shared case where the builtin decrefs internally. + +--- + +## Interpreter Contract + +The interpreter uses ownership metadata per builtin: + +| Argument Type | Interpreter Action After Call | +|---------------|-------------------------------| +| **Borrow** | Decref argument (release our copy) | +| **Consume** | Don't decref (ownership transferred to builtin) | + +This requires each low-level op to declare whether each argument is borrowed or consumed. +See `src/check/lower_ops.zig` for the ownership metadata. + +--- + +## Standard Terminology + +| Term | Definition | +|------|------------| +| **Borrow** | Function reads argument without affecting refcount. Caller retains ownership. | +| **Consume** | Function takes ownership of argument. Caller loses access. Function handles cleanup. | +| **Copy-on-Write** | Consume variant: if unique, returns same allocation; if shared, decrefs and allocates new. | +| **Seamless Slice** | Result shares underlying data with argument. Builtin calls incref internally. | +| **Own** | The entity responsible for eventually calling decref. | +| **Unique** | Refcount == 1. Safe to mutate in place. | + +--- + +## Function Documentation Format + +Every builtin should document its ownership semantics: + +```zig +/// Brief description of what the function does. +/// +/// ## Ownership +/// - `arg1`: **consumes** - caller loses ownership +/// - `arg2`: **borrows** - caller retains ownership +/// - Returns: **independent** / **copy-on-write** / **seamless-slice** +/// +/// ## Notes +/// Additional implementation details relevant to callers. +pub fn exampleFunction(...) ReturnType { ... } +``` + +--- + +## Function Categories + +### str.zig + +**Borrow args, Independent result:** +- `strEqual` - borrows both → Bool +- `strContains` - borrows both → Bool +- `strStartsWith` / `strEndsWith` - borrows both → Bool +- `strNumberOfBytes` / `countUtf8Bytes` - borrows → U64 + +**Consume arg, Copy-on-Write result:** +- `strWithAsciiUppercased` - consumes → Str (same-if-unique) +- `strWithAsciiLowercased` - consumes → Str (same-if-unique) + +**Consume arg, Seamless-slice OR Copy-on-Write result:** +- `strTrim` / `strTrimStart` / `strTrimEnd` - consumes → Str + - If unique with no offset needed: shrinks in place (copy-on-write) + - Otherwise: creates consuming seamless slice (inherits reference) + +**Consume args, Independent result:** +- `strConcat` - consumes first, borrows second → new Str +- `strJoinWith` - consumes list, borrows separator → new Str + +**Borrow arg, Seamless-slice result (incref'd):** +- `strToUtf8` - borrows → List (seamless slice, calls incref) +- `strDropPrefix` / `strDropSuffix` - borrows → Str (seamless slice or incref'd original) + +**Borrow arg, Seamless-slice result (no incref - caller must handle):** +- `substringUnsafe` - borrows → Str (seamless slice, NO incref!) + - **WARNING**: Caller is responsible for refcount management + +### list.zig + +**Borrow args, Independent result:** +- `listLen` - borrows → U64 +- `listIsEmpty` - borrows → Bool +- `listGetUnsafe` - borrows → pointer (no ownership transfer) + +**Consume args, Copy-on-Write result:** +- `listAppend` - consumes list, borrows element → List (same-if-unique) +- `listPrepend` - consumes list, borrows element → List (same-if-unique) + +**Consume args, Independent result:** +- `listConcat` - consumes both → new List +- `listMap` / `listKeepIf` / `listDropIf` - consumes → new List + +**Consume arg, Seamless-slice OR Copy-on-Write result:** +- `listSublist` - consumes → List + - If unique at start: shrinks in place (copy-on-write) + - Otherwise: creates consuming seamless slice (inherits reference) + +### box.zig (interpreter intrinsics) + +**Consume arg, Independent result:** +- `Box.box` - consumes value → Box (heap-allocated copy) + - Copies value data to heap with refcount = 1 + - Original value is decremented after copy +- `Box.unbox` - consumes Box → value (stack copy) + - Copies boxed data from heap to stack + - Box is decremented after copy (may free heap memory if refcount reaches 0) + - If boxed value is refcounted, its refcount is incremented (new reference created) diff --git a/src/builtins/dec.zig b/src/builtins/dec.zig index cb0674a637..8e09a54c3a 100644 --- a/src/builtins/dec.zig +++ b/src/builtins/dec.zig @@ -172,7 +172,7 @@ pub const RocDec = extern struct { // Format the backing i128 into an array of digit (ascii) characters (u8s) var digit_bytes_storage: [max_digits + 1]u8 = undefined; - var num_digits = std.fmt.formatIntBuf(digit_bytes_storage[0..], num, 10, .lower, .{}); + var num_digits = std.fmt.printInt(digit_bytes_storage[0..], num, 10, .lower, .{}); var digit_bytes: [*]u8 = digit_bytes_storage[0..]; // space where we assemble all the characters that make up the final string @@ -663,6 +663,20 @@ pub const RocDec = extern struct { pub fn atan(self: RocDec) RocDec { return fromF64(math.atan(self.toF64())).?; } + + pub fn rem( + self: RocDec, + other: RocDec, + roc_ops: *RocOps, + ) RocDec { + // (n % 0) is an error + if (other.num == 0) { + roc_ops.crash("Decimal remainder by 0!"); + } + + // For Dec, remainder is straightforward since both operands have the same scaling factor + return RocDec{ .num = @rem(self.num, other.num) }; + } }; // A number has `k` trailing zeros if `10^k` divides into it cleanly @@ -996,7 +1010,7 @@ const expect = std.testing.expect; // exports /// TODO: Document fromStr. -pub fn fromStr(arg: RocStr) callconv(.C) NumParseResult(i128) { +pub fn fromStr(arg: RocStr) callconv(.c) NumParseResult(i128) { if (@call(.always_inline, RocDec.fromStr, .{arg})) |dec| { return .{ .errorcode = 0, .value = dec.num }; } else { @@ -1008,7 +1022,7 @@ pub fn fromStr(arg: RocStr) callconv(.C) NumParseResult(i128) { pub fn to_str( arg: RocDec, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { return @call(.always_inline, RocDec.to_str, .{ arg, roc_ops }); } @@ -1016,7 +1030,7 @@ pub fn to_str( pub fn fromF64C( arg: f64, roc_ops: *RocOps, -) callconv(.C) i128 { +) callconv(.c) i128 { if (@call(.always_inline, RocDec.fromF64, .{arg})) |dec| { return dec.num; } else { @@ -1029,7 +1043,7 @@ pub fn fromF64C( pub fn fromF32C( arg_f32: f32, roc_ops: *RocOps, -) callconv(.C) i128 { +) callconv(.c) i128 { const arg_f64 = arg_f32; if (@call(.always_inline, RocDec.fromF64, .{arg_f64})) |dec| { return dec.num; @@ -1040,17 +1054,59 @@ pub fn fromF32C( } /// TODO: Document toF64. -pub fn toF64(arg: RocDec) callconv(.C) f64 { +pub fn toF64(arg: RocDec) callconv(.c) f64 { return @call(.always_inline, RocDec.toF64, .{arg}); } +/// Convert Dec to F32 (lossy conversion) +pub fn toF32(arg: RocDec) callconv(.c) f32 { + return @floatCast(arg.toF64()); +} + +/// Convert Dec to F32 with range check - returns null if out of range +pub fn toF32Try(arg: RocDec) ?f32 { + const f64_val = arg.toF64(); + // Check if the value is within F32 range + if (f64_val > math.floatMax(f32) or f64_val < -math.floatMax(f32)) { + return null; + } + // Also check for infinity (which would indicate overflow) + const f32_val: f32 = @floatCast(f64_val); + if (math.isInf(f32_val) and !math.isInf(f64_val)) { + return null; + } + return f32_val; +} + +/// Convert Dec to integer by truncating the fractional part (wrapping on overflow) +pub fn toIntWrap(comptime T: type, arg: RocDec) T { + // Divide by one_point_zero_i128 to get the integer part + const whole_part = @divTrunc(arg.num, RocDec.one_point_zero_i128); + // Truncate to the target type (wrapping) + // First cast the i128 to u128, then truncate to the target size, then cast back to T if needed + const as_u128: u128 = @bitCast(whole_part); + const truncated = @as(std.meta.Int(.unsigned, @bitSizeOf(T)), @truncate(as_u128)); + return @bitCast(truncated); +} + +/// Convert Dec to integer by truncating the fractional part (returns null if out of range) +pub fn toIntTry(comptime T: type, arg: RocDec) ?T { + // Divide by one_point_zero_i128 to get the integer part + const whole_part = @divTrunc(arg.num, RocDec.one_point_zero_i128); + // Check if it fits in the target type + if (whole_part < math.minInt(T) or whole_part > math.maxInt(T)) { + return null; + } + return @intCast(whole_part); +} + /// TODO: Document exportFromInt. pub fn exportFromInt(comptime T: type, comptime name: []const u8) void { const f = struct { fn func( self: T, roc_ops: *RocOps, - ) callconv(.C) i128 { + ) callconv(.c) i128 { const this = @as(i128, @intCast(self)); const answer = @mulWithOverflow(this, RocDec.one_point_zero_i128); @@ -1065,27 +1121,27 @@ pub fn exportFromInt(comptime T: type, comptime name: []const u8) void { } /// TODO: Document fromU64C. -pub fn fromU64C(arg: u64) callconv(.C) i128 { +pub fn fromU64C(arg: u64) callconv(.c) i128 { return @call(.always_inline, RocDec.fromU64, .{arg}).toI128(); } /// TODO: Document toI128. -pub fn toI128(arg: RocDec) callconv(.C) i128 { +pub fn toI128(arg: RocDec) callconv(.c) i128 { return @call(.always_inline, RocDec.toI128, .{arg}); } /// TODO: Document fromI128. -pub fn fromI128(arg: i128) callconv(.C) RocDec { +pub fn fromI128(arg: i128) callconv(.c) RocDec { return @call(.always_inline, RocDec.fromI128, .{arg}); } /// TODO: Document eqC. -pub fn eqC(arg1: RocDec, arg2: RocDec) callconv(.C) bool { +pub fn eqC(arg1: RocDec, arg2: RocDec) callconv(.c) bool { return @call(.always_inline, RocDec.eq, .{ arg1, arg2 }); } /// TODO: Document neqC. -pub fn neqC(arg1: RocDec, arg2: RocDec) callconv(.C) bool { +pub fn neqC(arg1: RocDec, arg2: RocDec) callconv(.c) bool { return @call(.always_inline, RocDec.neq, .{ arg1, arg2 }); } @@ -1093,7 +1149,7 @@ pub fn neqC(arg1: RocDec, arg2: RocDec) callconv(.C) bool { pub fn negateC( arg: RocDec, roc_ops: *RocOps, -) callconv(.C) i128 { +) callconv(.c) i128 { return if (@call(.always_inline, RocDec.negate, .{arg})) |dec| dec.num else { roc_ops.crash("Decimal negation overflow!"); unreachable; @@ -1104,7 +1160,7 @@ pub fn negateC( pub fn absC( arg: RocDec, roc_ops: *RocOps, -) callconv(.C) i128 { +) callconv(.c) i128 { const result = @call(.always_inline, RocDec.abs, .{arg}) catch { roc_ops.crash("Decimal absolute value overflow!"); unreachable; @@ -1113,17 +1169,17 @@ pub fn absC( } /// TODO: Document addC. -pub fn addC(arg1: RocDec, arg2: RocDec) callconv(.C) WithOverflow(RocDec) { +pub fn addC(arg1: RocDec, arg2: RocDec) callconv(.c) WithOverflow(RocDec) { return @call(.always_inline, RocDec.addWithOverflow, .{ arg1, arg2 }); } /// TODO: Document subC. -pub fn subC(arg1: RocDec, arg2: RocDec) callconv(.C) WithOverflow(RocDec) { +pub fn subC(arg1: RocDec, arg2: RocDec) callconv(.c) WithOverflow(RocDec) { return @call(.always_inline, RocDec.subWithOverflow, .{ arg1, arg2 }); } /// TODO: Document mulC. -pub fn mulC(arg1: RocDec, arg2: RocDec) callconv(.C) WithOverflow(RocDec) { +pub fn mulC(arg1: RocDec, arg2: RocDec) callconv(.c) WithOverflow(RocDec) { return @call(.always_inline, RocDec.mulWithOverflow, .{ arg1, arg2 }); } @@ -1132,12 +1188,12 @@ pub fn divC( arg1: RocDec, arg2: RocDec, roc_ops: *RocOps, -) callconv(.C) i128 { +) callconv(.c) i128 { return @call(.always_inline, RocDec.div, .{ arg1, arg2, roc_ops }).num; } /// TODO: Document logC. -pub fn logC(arg: RocDec) callconv(.C) i128 { +pub fn logC(arg: RocDec) callconv(.c) i128 { return @call(.always_inline, RocDec.log, .{arg}).num; } @@ -1146,37 +1202,37 @@ pub fn powC( arg1: RocDec, arg2: RocDec, roc_ops: *RocOps, -) callconv(.C) i128 { +) callconv(.c) i128 { return @call(.always_inline, RocDec.pow, .{ arg1, arg2, roc_ops }).num; } /// TODO: Document sinC. -pub fn sinC(arg: RocDec) callconv(.C) i128 { +pub fn sinC(arg: RocDec) callconv(.c) i128 { return @call(.always_inline, RocDec.sin, .{arg}).num; } /// TODO: Document cosC. -pub fn cosC(arg: RocDec) callconv(.C) i128 { +pub fn cosC(arg: RocDec) callconv(.c) i128 { return @call(.always_inline, RocDec.cos, .{arg}).num; } /// TODO: Document tanC. -pub fn tanC(arg: RocDec) callconv(.C) i128 { +pub fn tanC(arg: RocDec) callconv(.c) i128 { return @call(.always_inline, RocDec.tan, .{arg}).num; } /// TODO: Document asinC. -pub fn asinC(arg: RocDec) callconv(.C) i128 { +pub fn asinC(arg: RocDec) callconv(.c) i128 { return @call(.always_inline, RocDec.asin, .{arg}).num; } /// TODO: Document acosC. -pub fn acosC(arg: RocDec) callconv(.C) i128 { +pub fn acosC(arg: RocDec) callconv(.c) i128 { return @call(.always_inline, RocDec.acos, .{arg}).num; } /// TODO: Document atanC. -pub fn atanC(arg: RocDec) callconv(.C) i128 { +pub fn atanC(arg: RocDec) callconv(.c) i128 { return @call(.always_inline, RocDec.atan, .{arg}).num; } @@ -1185,12 +1241,12 @@ pub fn addOrPanicC( arg1: RocDec, arg2: RocDec, roc_ops: *RocOps, -) callconv(.C) RocDec { +) callconv(.c) RocDec { return @call(.always_inline, RocDec.add, .{ arg1, arg2, roc_ops }); } /// TODO: Document addSaturatedC. -pub fn addSaturatedC(arg1: RocDec, arg2: RocDec) callconv(.C) RocDec { +pub fn addSaturatedC(arg1: RocDec, arg2: RocDec) callconv(.c) RocDec { return @call(.always_inline, RocDec.addSaturated, .{ arg1, arg2 }); } @@ -1199,12 +1255,12 @@ pub fn subOrPanicC( arg1: RocDec, arg2: RocDec, roc_ops: *RocOps, -) callconv(.C) RocDec { +) callconv(.c) RocDec { return @call(.always_inline, RocDec.sub, .{ arg1, arg2, roc_ops }); } /// TODO: Document subSaturatedC. -pub fn subSaturatedC(arg1: RocDec, arg2: RocDec) callconv(.C) RocDec { +pub fn subSaturatedC(arg1: RocDec, arg2: RocDec) callconv(.c) RocDec { return @call(.always_inline, RocDec.subSaturated, .{ arg1, arg2 }); } @@ -1213,12 +1269,12 @@ pub fn mulOrPanicC( arg1: RocDec, arg2: RocDec, roc_ops: *RocOps, -) callconv(.C) RocDec { +) callconv(.c) RocDec { return @call(.always_inline, RocDec.mul, .{ arg1, arg2, roc_ops }); } /// TODO: Document mulSaturatedC. -pub fn mulSaturatedC(arg1: RocDec, arg2: RocDec) callconv(.C) RocDec { +pub fn mulSaturatedC(arg1: RocDec, arg2: RocDec) callconv(.c) RocDec { return @call(.always_inline, RocDec.mulSaturated, .{ arg1, arg2 }); } @@ -1228,7 +1284,7 @@ pub fn exportRound(comptime T: type, comptime name: []const u8) void { fn func( input: RocDec, roc_ops: *RocOps, - ) callconv(.C) T { + ) callconv(.c) T { return @as(T, @intCast(@divFloor(input.round(roc_ops).num, RocDec.one_point_zero_i128))); } }.func; @@ -1241,7 +1297,7 @@ pub fn exportFloor(comptime T: type, comptime name: []const u8) void { fn func( input: RocDec, roc_ops: *RocOps, - ) callconv(.C) T { + ) callconv(.c) T { return @as(T, @intCast(@divFloor(input.floor(roc_ops).num, RocDec.one_point_zero_i128))); } }.func; @@ -1254,7 +1310,7 @@ pub fn exportCeiling(comptime T: type, comptime name: []const u8) void { fn func( input: RocDec, roc_ops: *RocOps, - ) callconv(.C) T { + ) callconv(.c) T { return @as(T, @intCast(@divFloor(input.ceiling(roc_ops).num, RocDec.one_point_zero_i128))); } }.func; diff --git a/src/builtins/fuzz_sort.zig b/src/builtins/fuzz_sort.zig index b2e178ea71..55d56c35ee 100644 --- a/src/builtins/fuzz_sort.zig +++ b/src/builtins/fuzz_sort.zig @@ -7,7 +7,7 @@ const std = @import("std"); const sort = @import("sort.zig"); -fn cMain() callconv(.C) i32 { +fn cMain() callconv(.c) i32 { fuzz_main() catch unreachable; return 0; } @@ -29,12 +29,12 @@ pub fn fuzz_main() !void { allocator = gpa.allocator(); // Read the data from stdin - const stdin = std.io.getStdIn(); + const stdin = std.fs.File.stdin(); const data = try stdin.readToEndAlloc(allocator, std.math.maxInt(usize)); defer allocator.free(data); const len = data.len / @sizeOf(i64); - const arr_ptr: [*]i64 = @alignCast(@ptrCast(data.ptr)); + const arr_ptr: [*]i64 = @ptrCast(@alignCast(data.ptr)); if (DEBUG) { std.debug.print("Input: [{d}]{d}\n", .{ len, arr_ptr[0..len] }); @@ -52,9 +52,9 @@ pub fn fuzz_main() !void { } const Opaque = ?[*]u8; -fn test_i64_compare_refcounted(count_ptr: Opaque, a_ptr: Opaque, b_ptr: Opaque) callconv(.C) u8 { - const a = @as(*i64, @alignCast(@ptrCast(a_ptr))).*; - const b = @as(*i64, @alignCast(@ptrCast(b_ptr))).*; +fn test_i64_compare_refcounted(count_ptr: Opaque, a_ptr: Opaque, b_ptr: Opaque) callconv(.c) u8 { + const a = @as(*i64, @ptrCast(@alignCast(a_ptr))).*; + const b = @as(*i64, @ptrCast(@alignCast(b_ptr))).*; const gt = @as(u8, @intFromBool(a > b)); const lt = @as(u8, @intFromBool(a < b)); @@ -67,11 +67,11 @@ fn test_i64_compare_refcounted(count_ptr: Opaque, a_ptr: Opaque, b_ptr: Opaque) return lt + lt + gt; } -fn test_i64_copy(dst_ptr: Opaque, src_ptr: Opaque) callconv(.C) void { - @as(*i64, @alignCast(@ptrCast(dst_ptr))).* = @as(*i64, @alignCast(@ptrCast(src_ptr))).*; +fn test_i64_copy(dst_ptr: Opaque, src_ptr: Opaque) callconv(.c) void { + @as(*i64, @ptrCast(@alignCast(dst_ptr))).* = @as(*i64, @ptrCast(@alignCast(src_ptr))).*; } -fn test_inc_n_data(count_ptr: Opaque, n: usize) callconv(.C) void { +fn test_inc_n_data(count_ptr: Opaque, n: usize) callconv(.c) void { @as(*isize, @ptrCast(@alignCast(count_ptr))).* += @intCast(n); } @@ -81,25 +81,22 @@ comptime { @export(&testing_roc_panic, .{ .name = "roc_panic", .linkage = .Strong }); } -fn testing_roc_alloc(size: usize, _: u32) callconv(.C) ?*anyopaque { +fn testing_roc_alloc(size: usize, _: u32) callconv(.c) ?*anyopaque { // We store an extra usize which is the size of the full allocation. const full_size = size + @sizeOf(usize); var raw_ptr = (allocator.alloc(u8, full_size) catch unreachable).ptr; - @as([*]usize, @alignCast(@ptrCast(raw_ptr)))[0] = full_size; + @as([*]usize, @ptrCast(@alignCast(raw_ptr)))[0] = full_size; raw_ptr += @sizeOf(usize); return @as(?*anyopaque, @ptrCast(raw_ptr)); } -fn testing_roc_dealloc(c_ptr: *anyopaque, _: u32) callconv(.C) void { +fn testing_roc_dealloc(c_ptr: *anyopaque, _: u32) callconv(.c) void { const raw_ptr = @as([*]u8, @ptrCast(c_ptr)) - @sizeOf(usize); - const full_size = @as([*]usize, @alignCast(@ptrCast(raw_ptr)))[0]; + const full_size = @as([*]usize, @ptrCast(@alignCast(raw_ptr)))[0]; const slice = raw_ptr[0..full_size]; allocator.free(slice); } -fn testing_roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { - _ = c_ptr; - _ = tag_id; - +fn testing_roc_panic(_: *anyopaque, _: u32) callconv(.c) void { @panic("Roc panicked"); } diff --git a/src/builtins/handlers.zig b/src/builtins/handlers.zig new file mode 100644 index 0000000000..dd7617af76 --- /dev/null +++ b/src/builtins/handlers.zig @@ -0,0 +1,338 @@ +//! Generic signal handlers for stack overflow, access violation, and arithmetic errors. +//! +//! This module provides a mechanism to catch runtime errors like stack overflows, +//! access violations, and division by zero, handling them with custom callbacks +//! instead of crashing with a raw signal. +//! +//! On POSIX systems (Linux, macOS), we use sigaltstack to set up an alternate +//! signal stack and install handlers for SIGSEGV, SIGBUS, and SIGFPE. +//! +//! On Windows, we use SetUnhandledExceptionFilter to catch various exceptions. +//! +//! Freestanding targets (like wasm32) are not supported (no signal handling available). + +const std = @import("std"); +const builtin = @import("builtin"); +const posix = if (builtin.os.tag != .windows and builtin.os.tag != .freestanding) std.posix else undefined; + +// Windows types and constants +const DWORD = u32; +const LONG = i32; +const ULONG_PTR = usize; +const PVOID = ?*anyopaque; +const HANDLE = ?*anyopaque; +const BOOL = i32; + +const EXCEPTION_STACK_OVERFLOW: DWORD = 0xC00000FD; +const EXCEPTION_ACCESS_VIOLATION: DWORD = 0xC0000005; +const EXCEPTION_INT_DIVIDE_BY_ZERO: DWORD = 0xC0000094; +const EXCEPTION_INT_OVERFLOW: DWORD = 0xC0000095; +const EXCEPTION_CONTINUE_SEARCH: LONG = 0; +const STD_ERROR_HANDLE: DWORD = @bitCast(@as(i32, -12)); +const INVALID_HANDLE_VALUE: HANDLE = @ptrFromInt(std.math.maxInt(usize)); + +const EXCEPTION_RECORD = extern struct { + ExceptionCode: DWORD, + ExceptionFlags: DWORD, + ExceptionRecord: ?*EXCEPTION_RECORD, + ExceptionAddress: PVOID, + NumberParameters: DWORD, + ExceptionInformation: [15]ULONG_PTR, +}; + +const CONTEXT = extern struct { + // We don't need the full context, just enough to make the struct valid + data: [1232]u8, // Size varies by arch, this is x64 size +}; + +const EXCEPTION_POINTERS = extern struct { + ExceptionRecord: *EXCEPTION_RECORD, + ContextRecord: *CONTEXT, +}; + +const LPTOP_LEVEL_EXCEPTION_FILTER = ?*const fn (*EXCEPTION_POINTERS) callconv(.winapi) LONG; + +// Windows API imports +extern "kernel32" fn SetUnhandledExceptionFilter(lpTopLevelExceptionFilter: LPTOP_LEVEL_EXCEPTION_FILTER) callconv(.winapi) LPTOP_LEVEL_EXCEPTION_FILTER; +extern "kernel32" fn GetStdHandle(nStdHandle: DWORD) callconv(.winapi) HANDLE; +extern "kernel32" fn WriteFile(hFile: HANDLE, lpBuffer: [*]const u8, nNumberOfBytesToWrite: DWORD, lpNumberOfBytesWritten: ?*DWORD, lpOverlapped: ?*anyopaque) callconv(.winapi) BOOL; +extern "kernel32" fn ExitProcess(uExitCode: c_uint) callconv(.winapi) noreturn; + +/// Size of the alternate signal stack (64KB should be plenty for the handler) +const ALT_STACK_SIZE = 64 * 1024; + +/// Storage for the alternate signal stack (POSIX only) +var alt_stack_storage: [ALT_STACK_SIZE]u8 align(16) = undefined; + +/// Whether the handler has been installed +var handler_installed = false; + +/// Callback function type for handling stack overflow +pub const StackOverflowCallback = *const fn () noreturn; + +/// Callback function type for handling access violation/segfault +pub const AccessViolationCallback = *const fn (fault_addr: usize) noreturn; + +/// Callback function type for handling division by zero (and other arithmetic errors) +pub const ArithmeticErrorCallback = *const fn () noreturn; + +/// Stored callbacks (set during install) +var stack_overflow_callback: ?StackOverflowCallback = null; +var access_violation_callback: ?AccessViolationCallback = null; +var arithmetic_error_callback: ?ArithmeticErrorCallback = null; + +/// Install signal handlers with custom callbacks. +/// +/// Parameters: +/// - on_stack_overflow: Called when a stack overflow is detected. Must not return. +/// - on_access_violation: Called for other memory access violations (segfaults). +/// Receives the fault address. Must not return. +/// - on_arithmetic_error: Called for arithmetic errors like division by zero. Must not return. +/// +/// Returns true if the handlers were installed successfully, false otherwise. +pub fn install( + on_stack_overflow: StackOverflowCallback, + on_access_violation: AccessViolationCallback, + on_arithmetic_error: ArithmeticErrorCallback, +) bool { + if (handler_installed) return true; + + stack_overflow_callback = on_stack_overflow; + access_violation_callback = on_access_violation; + arithmetic_error_callback = on_arithmetic_error; + + if (comptime builtin.os.tag == .windows) { + return installWindows(); + } + + if (comptime builtin.os.tag == .freestanding) { + // Freestanding targets (like wasm32) don't support signal handling + return false; + } + + return installPosix(); +} + +fn installPosix() bool { + // Set up the alternate signal stack + var alt_stack = posix.stack_t{ + .sp = &alt_stack_storage, + .flags = 0, + .size = ALT_STACK_SIZE, + }; + + posix.sigaltstack(&alt_stack, null) catch { + return false; + }; + + // Install the SIGSEGV handler for stack overflow and access violations + const segv_action = posix.Sigaction{ + .handler = .{ .sigaction = handleSegvSignal }, + .mask = posix.sigemptyset(), + .flags = posix.SA.SIGINFO | posix.SA.ONSTACK, + }; + + posix.sigaction(posix.SIG.SEGV, &segv_action, null); + + // Also catch SIGBUS which can occur on some systems for stack overflow + posix.sigaction(posix.SIG.BUS, &segv_action, null); + + // Install the SIGFPE handler for division by zero and other arithmetic errors + const fpe_action = posix.Sigaction{ + .handler = .{ .sigaction = handleFpeSignal }, + .mask = posix.sigemptyset(), + .flags = posix.SA.SIGINFO | posix.SA.ONSTACK, + }; + + posix.sigaction(posix.SIG.FPE, &fpe_action, null); + + handler_installed = true; + return true; +} + +fn installWindows() bool { + _ = SetUnhandledExceptionFilter(handleExceptionWindows); + handler_installed = true; + return true; +} + +/// Windows exception handler function +fn handleExceptionWindows(exception_info: *EXCEPTION_POINTERS) callconv(.winapi) LONG { + const exception_code = exception_info.ExceptionRecord.ExceptionCode; + + // Check if this is a known exception type + const is_stack_overflow = (exception_code == EXCEPTION_STACK_OVERFLOW); + const is_access_violation = (exception_code == EXCEPTION_ACCESS_VIOLATION); + const is_divide_by_zero = (exception_code == EXCEPTION_INT_DIVIDE_BY_ZERO); + const is_int_overflow = (exception_code == EXCEPTION_INT_OVERFLOW); + const is_arithmetic_error = is_divide_by_zero or is_int_overflow; + + if (!is_stack_overflow and !is_access_violation and !is_arithmetic_error) { + // Let other handlers deal with this exception + return EXCEPTION_CONTINUE_SEARCH; + } + + if (is_stack_overflow) { + if (stack_overflow_callback) |callback| { + callback(); + } + ExitProcess(134); + } else if (is_arithmetic_error) { + if (arithmetic_error_callback) |callback| { + callback(); + } + ExitProcess(136); // 128 + 8 (SIGFPE) + } else { + if (access_violation_callback) |callback| { + // Get fault address from ExceptionInformation[1] for access violations + const fault_addr = exception_info.ExceptionRecord.ExceptionInformation[1]; + callback(fault_addr); + } + ExitProcess(139); + } +} + +/// The POSIX SIGSEGV/SIGBUS signal handler function +fn handleSegvSignal(_: i32, info: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) void { + // Get the fault address - access differs by platform + const fault_addr: usize = getFaultAddress(info); + + // Get the current stack pointer to help determine if this is a stack overflow + var current_sp: usize = 0; + asm volatile ("" + : [sp] "={sp}" (current_sp), + ); + + // A stack overflow typically occurs when the fault address is near the stack pointer + // or below the stack (stacks grow downward on most architectures) + const likely_stack_overflow = isLikelyStackOverflow(fault_addr, current_sp); + + if (likely_stack_overflow) { + if (stack_overflow_callback) |callback| { + callback(); + } + } else { + if (access_violation_callback) |callback| { + callback(fault_addr); + } + } + + // If no callback was set, exit with appropriate code + if (likely_stack_overflow) { + posix.exit(134); // 128 + 6 (SIGABRT-like) + } else { + posix.exit(139); // 128 + 11 (SIGSEGV) + } +} + +/// The POSIX SIGFPE signal handler function (division by zero, etc.) +fn handleFpeSignal(_: i32, _: *const posix.siginfo_t, _: ?*anyopaque) callconv(.c) void { + if (arithmetic_error_callback) |callback| { + callback(); + } + + // If no callback was set, exit with SIGFPE code + posix.exit(136); // 128 + 8 (SIGFPE) +} + +/// Get the fault address from siginfo_t (platform-specific) +fn getFaultAddress(info: *const posix.siginfo_t) usize { + // The siginfo_t structure varies by platform + if (comptime builtin.os.tag == .linux) { + // Linux: fault address is in fields.sigfault.addr + return @intFromPtr(info.fields.sigfault.addr); + } else if (comptime builtin.os.tag == .macos or + builtin.os.tag == .ios or + builtin.os.tag == .tvos or + builtin.os.tag == .watchos or + builtin.os.tag == .visionos or + builtin.os.tag == .freebsd or + builtin.os.tag == .dragonfly or + builtin.os.tag == .netbsd or + builtin.os.tag == .openbsd) + { + // macOS/iOS/BSD: fault address is in addr field + return @intFromPtr(info.addr); + } else { + // Fallback: return 0 if we can't determine the address + return 0; + } +} + +/// Heuristic to determine if a fault is likely a stack overflow +fn isLikelyStackOverflow(fault_addr: usize, current_sp: usize) bool { + // If fault address is 0 or very low, it's likely a null pointer dereference + if (fault_addr < 4096) return false; + + // If the fault address is close to the current stack pointer (within 16MB), + // it's very likely a stack overflow. The signal handler runs on an alternate + // stack, but the fault address should still be near where the stack was. + const sp_distance = if (fault_addr < current_sp) current_sp - fault_addr else fault_addr - current_sp; + if (sp_distance < 16 * 1024 * 1024) { // Within 16MB of stack pointer + return true; + } + + // On 64-bit systems, stacks are typically placed in high memory. + // On macOS, the stack is around 0x16XXXXXXXX (about 6GB mark). + // On Linux, it's typically near 0x7FFFFFFFFFFF. + // If the fault address is in the upper half of the address space, + // it's more likely to be a stack-related issue. + if (comptime @sizeOf(usize) == 8) { + // 64-bit: check if address is in upper portion of address space + // On macOS, stacks start around 0x100000000 (4GB) and go up + // On Linux, stacks are near 0x7FFFFFFFFFFF + const lower_bound: usize = 0x100000000; // 4GB + if (fault_addr > lower_bound) { + // This is in the region where stacks typically are on 64-bit systems + // Default to assuming it's a stack overflow for addresses in this range + return true; + } + } else { + // 32-bit: stacks are typically in the upper portion of the 4GB space + const lower_bound: usize = 0x40000000; // 1GB + if (fault_addr > lower_bound) { + return true; + } + } + + return false; +} + +/// Format a usize as hexadecimal (for use in callbacks) +pub fn formatHex(value: usize, buf: []u8) []const u8 { + const hex_chars = "0123456789abcdef"; + var i: usize = buf.len; + + if (value == 0) { + i -= 1; + buf[i] = '0'; + } else { + var v = value; + while (v > 0 and i > 2) { + i -= 1; + buf[i] = hex_chars[v & 0xf]; + v >>= 4; + } + } + + // Add 0x prefix + i -= 1; + buf[i] = 'x'; + i -= 1; + buf[i] = '0'; + + return buf[i..]; +} + +test "formatHex" { + var buf: [18]u8 = undefined; + + const zero = formatHex(0, &buf); + try std.testing.expectEqualStrings("0x0", zero); + + const small = formatHex(0xff, &buf); + try std.testing.expectEqualStrings("0xff", small); + + const medium = formatHex(0xdeadbeef, &buf); + try std.testing.expectEqualStrings("0xdeadbeef", medium); +} diff --git a/src/builtins/hash.zig b/src/builtins/hash.zig index b14949cccf..592b5ddcfc 100644 --- a/src/builtins/hash.zig +++ b/src/builtins/hash.zig @@ -10,7 +10,7 @@ const str = @import("str.zig"); const mem = std.mem; /// TODO: Document wyhash. -pub fn wyhash(seed: u64, bytes: ?[*]const u8, length: usize) callconv(.C) u64 { +pub fn wyhash(seed: u64, bytes: ?[*]const u8, length: usize) callconv(.c) u64 { if (bytes) |nonnull| { const slice = nonnull[0..length]; return wyhash_hash(seed, slice); @@ -20,7 +20,7 @@ pub fn wyhash(seed: u64, bytes: ?[*]const u8, length: usize) callconv(.C) u64 { } /// TODO: Document wyhash_rocstr. -pub fn wyhash_rocstr(seed: u64, input: str.RocStr) callconv(.C) u64 { +pub fn wyhash_rocstr(seed: u64, input: str.RocStr) callconv(.c) u64 { return wyhash_hash(seed, input.asSlice()); } diff --git a/src/builtins/host_abi.zig b/src/builtins/host_abi.zig index eed80a83d7..6782fd0198 100644 --- a/src/builtins/host_abi.zig +++ b/src/builtins/host_abi.zig @@ -9,6 +9,8 @@ //! This design makes Roc's ABI very simple; the calling convention is just "Ops pointer, //! return pointer, args pointers". +const tracy = @import("tracy"); + /// todo: describe RocCall pub const RocCall = fn ( /// Function pointers that the Roc program uses, e.g. alloc, dealloc, etc. @@ -25,13 +27,26 @@ pub const RocCall = fn ( /// /// For a zig host use an `extern struct` for well-defined in-memory layout matching the C ABI for the target *anyopaque, -) callconv(.C) void; +) callconv(.c) void; /// The operations (in the form of function pointers) that a running Roc program /// needs the host to provide. /// /// This is used in both calls from actual hosts as well as evaluation of constants /// inside the Roc compiler itself. +/// Function pointer type for hosted functions provided by the platform. +/// All hosted functions follow the RocCall ABI: (ops, ret_ptr, args_ptr). +pub const HostedFn = *const fn (*RocOps, *anyopaque, *anyopaque) callconv(.c) void; + +/// Array of hosted function pointers provided by the platform. +/// These are sorted alphabetically by function name during canonicalization. +pub const HostedFunctions = extern struct { + count: u32, + fns: [*]HostedFn, +}; + +/// Operations that the host provides to Roc code, including memory management, +/// panic handling, and platform-specific effects. pub const RocOps = extern struct { /// The host provides this pointer, and Roc passes it to each of the following /// function pointers as a second argument. This lets the host do things like use @@ -39,25 +54,27 @@ pub const RocOps = extern struct { /// The pointer can be to absolutely anything the host likes, or null if unused. env: *anyopaque, /// Similar to _aligned_malloc - https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/aligned-malloc - roc_alloc: *const fn (*RocAlloc, *anyopaque) callconv(.C) void, + roc_alloc: *const fn (*RocAlloc, *anyopaque) callconv(.c) void, /// Similar to _aligned_free - https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/aligned-free - roc_dealloc: *const fn (*RocDealloc, *anyopaque) callconv(.C) void, + roc_dealloc: *const fn (*RocDealloc, *anyopaque) callconv(.c) void, /// Similar to _aligned_realloc - https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/aligned-realloc - roc_realloc: *const fn (*RocRealloc, *anyopaque) callconv(.C) void, + roc_realloc: *const fn (*RocRealloc, *anyopaque) callconv(.c) void, /// Called when the Roc program has called `dbg` on something. - roc_dbg: *const fn (*const RocDbg, *anyopaque) callconv(.C) void, + roc_dbg: *const fn (*const RocDbg, *anyopaque) callconv(.c) void, /// Called when the Roc program has run an `expect` which failed. - roc_expect_failed: *const fn (*const RocExpectFailed, *anyopaque) callconv(.C) void, + roc_expect_failed: *const fn (*const RocExpectFailed, *anyopaque) callconv(.c) void, /// Called when the Roc program crashes, e.g. due to integer overflow. /// The host should handle this gracefully and stop execution of the Roc program. - roc_crashed: *const fn (*const RocCrashed, *anyopaque) callconv(.C) void, - /// At the end of this struct, the host must include all the functions - /// it wants to provide to the Roc program for the Roc program to call - /// (e.g. I/O operations and such). - host_fns: *anyopaque, + roc_crashed: *const fn (*const RocCrashed, *anyopaque) callconv(.c) void, + /// Hosted functions provided by the platform (sorted alphabetically by name). + /// These are effectful operations like I/O that the platform provides to Type Modules. + hosted_fns: HostedFunctions, /// Helper function to crash the Roc program, returns control to the host. pub fn crash(self: *RocOps, msg: []const u8) void { + const trace = tracy.trace(@src()); + defer trace.end(); + const roc_crashed_args = RocCrashed{ .utf8_bytes = @constCast(msg.ptr), .len = msg.len, @@ -65,17 +82,44 @@ pub const RocOps = extern struct { self.roc_crashed(&roc_crashed_args, self.env); } + /// Helper function to send debug output to the host. + pub fn dbg(self: *RocOps, msg: []const u8) void { + const trace = tracy.trace(@src()); + defer trace.end(); + + const roc_dbg_args = RocDbg{ + .utf8_bytes = @constCast(msg.ptr), + .len = msg.len, + }; + self.roc_dbg(&roc_dbg_args, self.env); + } + pub fn alloc(self: *RocOps, alignment: usize, length: usize) *anyopaque { + const trace = tracy.trace(@src()); + defer trace.end(); + var roc_alloc_args = RocAlloc{ .alignment = alignment, .length = length, .answer = self.env, }; self.roc_alloc(&roc_alloc_args, self.env); + + if (tracy.enable_allocation) { + tracy.alloc(@ptrCast(roc_alloc_args.answer), length); + } + return roc_alloc_args.answer; } pub fn dealloc(self: *RocOps, ptr: *anyopaque, alignment: usize) void { + const trace = tracy.trace(@src()); + defer trace.end(); + + if (tracy.enable_allocation) { + tracy.free(@ptrCast(ptr)); + } + var roc_dealloc_args = RocDealloc{ .alignment = alignment, .ptr = ptr, diff --git a/src/builtins/list.zig b/src/builtins/list.zig index 63886d0d72..57b1e39772 100644 --- a/src/builtins/list.zig +++ b/src/builtins/list.zig @@ -2,7 +2,20 @@ //! //! Lists use copy-on-write semantics to minimize allocations when shared across contexts. //! Seamless slice optimization reduces memory overhead for substring operations. +//! +//! ## Ownership Semantics +//! +//! See `OWNERSHIP.md` for the canonical terminology. Functions in this module +//! follow these patterns: +//! +//! - **Borrow**: Function reads argument, caller retains ownership +//! - **Consume**: Function takes ownership, caller loses access +//! - **Copy-on-Write**: Consumes arg; if unique, mutates in place; if shared, allocates new +//! - **Seamless Slice**: Result shares data with arg via incref'd slice +//! +//! Each function documents its ownership semantics in its doc comment. const std = @import("std"); +const builtin = @import("builtin"); const utils = @import("utils.zig"); const UpdateMode = utils.UpdateMode; @@ -11,15 +24,18 @@ const RocOps = @import("host_abi.zig").RocOps; const RocStr = @import("str.zig").RocStr; const increfDataPtrC = utils.increfDataPtrC; -const Opaque = ?[*]u8; -const EqFn = *const fn (Opaque, Opaque) callconv(.C) bool; -const CompareFn = *const fn (Opaque, Opaque, Opaque) callconv(.C) u8; -const CopyFn = *const fn (Opaque, Opaque) callconv(.C) void; +/// Pointer to the bytes of a list element or similar data +pub const Opaque = ?[*]u8; +const EqFn = *const fn (Opaque, Opaque) callconv(.c) bool; +const CompareFn = *const fn (Opaque, Opaque, Opaque) callconv(.c) u8; +const CopyFn = *const fn (Opaque, Opaque) callconv(.c) void; +/// Function copying data between 2 Opaques with a slot for the element's width +pub const CopyFallbackFn = *const fn (Opaque, Opaque, usize) callconv(.c) void; -const Inc = *const fn (?[*]u8) callconv(.C) void; -const IncN = *const fn (?[*]u8, usize) callconv(.C) void; -const Dec = *const fn (?[*]u8) callconv(.C) void; -const HasTagId = *const fn (u16, ?[*]u8) callconv(.C) extern struct { matched: bool, data: ?[*]u8 }; +const Inc = *const fn (?*anyopaque, ?[*]u8) callconv(.c) void; +const IncN = *const fn (?*anyopaque, ?[*]u8, usize) callconv(.c) void; +const Dec = *const fn (?*anyopaque, ?[*]u8) callconv(.c) void; +const HasTagId = *const fn (u16, ?[*]u8) callconv(.c) extern struct { matched: bool, data: ?[*]u8 }; /// A bit mask were the only set bit is the bit indicating if the List is a seamless slice. pub const SEAMLESS_SLICE_BIT: usize = @@ -122,23 +138,37 @@ pub const RocList = extern struct { // The pointer is to just after the refcount. // For big lists, it just returns their bytes pointer. // For seamless slices, it returns the pointer stored in capacity_or_alloc_ptr. - pub fn getAllocationDataPtr(self: RocList) ?[*]u8 { + pub fn getAllocationDataPtr(self: RocList, roc_ops: *RocOps) ?[*]u8 { const list_alloc_ptr = @intFromPtr(self.bytes); const slice_alloc_ptr = self.capacity_or_alloc_ptr << 1; const slice_mask = self.seamlessSliceMask(); const alloc_ptr = (list_alloc_ptr & ~slice_mask) | (slice_alloc_ptr & slice_mask); + + // Verify the computed allocation pointer is properly aligned + if (comptime builtin.mode == .Debug) { + if (alloc_ptr != 0 and alloc_ptr % @alignOf(usize) != 0) { + var buf: [256]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, "getAllocationDataPtr: misaligned ptr=0x{x} (bytes=0x{x}, cap_or_alloc=0x{x}, is_slice={})", .{ alloc_ptr, list_alloc_ptr, self.capacity_or_alloc_ptr, self.isSeamlessSlice() }) catch "getAllocationDataPtr: misaligned ptr"; + roc_ops.crash(msg); + } + } + return @as(?[*]u8, @ptrFromInt(alloc_ptr)); } - // This function is only valid if the list has refcounted elements. - fn getAllocationElementCount(self: RocList) usize { - if (self.isSeamlessSlice()) { + // Returns the number of elements to decref when freeing this list's allocation. + // For seamless slices with refcounted elements, this reads the original allocation size from the heap. + // For non-refcounted elements or non-slices, just returns the list length. + pub fn getAllocationElementCount(self: RocList, elements_refcounted: bool, roc_ops: *RocOps) usize { + // Only read from heap (-2) for seamless slices with refcounted elements. + // The count is only written by setAllocationElementCount when elements_refcounted=true. + if (self.isSeamlessSlice() and elements_refcounted) { // Seamless slices always refer to an underlying allocation. - const alloc_ptr = self.getAllocationDataPtr() orelse unreachable; + const alloc_ptr = self.getAllocationDataPtr(roc_ops) orelse unreachable; // - 1 is refcount. // - 2 is size on heap. - const ptr = @as([*]usize, @ptrCast(@alignCast(alloc_ptr))) - 2; - return ptr[0]; + const ptr: [*]usize = utils.alignedPtrCast([*]usize, alloc_ptr, @src()); + return (ptr - 2)[0]; } else { return self.length; } @@ -146,26 +176,28 @@ pub const RocList = extern struct { // This needs to be called when creating seamless slices from unique list. // It will put the allocation size on the heap to enable the seamless slice to free the underlying allocation. - fn setAllocationElementCount(self: RocList, elements_refcounted: bool) void { + fn setAllocationElementCount(self: RocList, elements_refcounted: bool, roc_ops: *RocOps) void { if (elements_refcounted and !self.isSeamlessSlice()) { - // - 1 is refcount. - // - 2 is size on heap. - const ptr = @as([*]usize, @alignCast(@ptrCast(self.getAllocationDataPtr()))) - 2; - ptr[0] = self.length; + if (self.getAllocationDataPtr(roc_ops)) |alloc_ptr| { + // - 1 is refcount. + // - 2 is size on heap. + const ptr: [*]usize = utils.alignedPtrCast([*]usize, alloc_ptr, @src()); + (ptr - 2)[0] = self.length; + } } } - pub fn incref(self: RocList, amount: isize, elements_refcounted: bool) void { + pub fn incref(self: RocList, amount: isize, elements_refcounted: bool, roc_ops: *RocOps) void { // If the list is unique and not a seamless slice, the length needs to be store on the heap if the elements are refcounted. - if (elements_refcounted and self.isUnique() and !self.isSeamlessSlice()) { - if (self.getAllocationDataPtr()) |source| { + if (elements_refcounted and self.isUnique(roc_ops) and !self.isSeamlessSlice()) { + if (self.getAllocationDataPtr(roc_ops)) |source| { // - 1 is refcount. // - 2 is size on heap. - const ptr = @as([*]usize, @alignCast(@ptrCast(source))) - 2; - ptr[0] = self.length; + const ptr: [*]usize = utils.alignedPtrCast([*]usize, source, @src()); + (ptr - 2)[0] = self.length; } } - increfDataPtrC(self.getAllocationDataPtr(), amount); + increfDataPtrC(self.getAllocationDataPtr(roc_ops), amount, roc_ops); } pub fn decref( @@ -173,25 +205,26 @@ pub const RocList = extern struct { alignment: u32, element_width: usize, elements_refcounted: bool, + dec_context: ?*anyopaque, dec: Dec, roc_ops: *RocOps, ) void { // If unique, decref will free the list. Before that happens, all elements must be decremented. - if (elements_refcounted and self.isUnique()) { - if (self.getAllocationDataPtr()) |source| { - const count = self.getAllocationElementCount(); + if (elements_refcounted and self.isUnique(roc_ops)) { + if (self.getAllocationDataPtr(roc_ops)) |source| { + const count = self.getAllocationElementCount(elements_refcounted, roc_ops); var i: usize = 0; while (i < count) : (i += 1) { const element = source + i * element_width; - dec(element); + dec(dec_context, element); } } } // We use the raw capacity to ensure we always decrement the refcount of seamless slices. utils.decref( - self.getAllocationDataPtr(), + self.getAllocationDataPtr(roc_ops), self.capacity_or_alloc_ptr, alignment, elements_refcounted, @@ -200,21 +233,32 @@ pub const RocList = extern struct { } pub fn elements(self: RocList, comptime T: type) ?[*]T { - return @as(?[*]T, @ptrCast(@alignCast(self.bytes))); + if (self.bytes) |bytes| { + return utils.alignedPtrCast([*]T, bytes, @src()); + } + return null; } - pub fn isUnique(self: RocList) bool { - return utils.rcUnique(@bitCast(self.refcount())); + pub fn isUnique(self: RocList, roc_ops: *RocOps) bool { + return utils.rcUnique(@bitCast(self.refcount(roc_ops))); } - fn refcount(self: RocList) usize { + fn refcount(self: RocList, roc_ops: *RocOps) usize { + // Reduced debug output - only print on potential issues if (self.getCapacity() == 0 and !self.isSeamlessSlice()) { // the zero-capacity is Clone, copying it will not leak memory return 1; } - const ptr: [*]usize = @as([*]usize, @ptrCast(@alignCast(self.getAllocationDataPtr()))); - return (ptr - 1)[0]; + if (self.getAllocationDataPtr(roc_ops)) |non_null_ptr| { + const ptr: [*]usize = utils.alignedPtrCast([*]usize, non_null_ptr, @src()); + return (ptr - 1)[0]; + } else { + var buf: [128]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, "RocList.refcount: getAllocationDataPtr returned null (capacity={}, is_slice={})", .{ self.getCapacity(), self.isSeamlessSlice() }) catch "RocList.refcount: getAllocationDataPtr returned null"; + roc_ops.crash(msg); + unreachable; + } } pub fn makeUnique( @@ -222,18 +266,20 @@ pub const RocList = extern struct { alignment: u32, element_width: usize, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, + dec_context: ?*anyopaque, dec: Dec, roc_ops: *RocOps, ) RocList { - if (self.isUnique()) { + if (self.isUnique(roc_ops)) { 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, element_width, elements_refcounted, dec, roc_ops); + self.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); return RocList.empty(); } @@ -250,11 +296,11 @@ pub const RocList = extern struct { if (elements_refcounted) { var i: usize = 0; while (i < self.len()) : (i += 1) { - inc(new_bytes + i * element_width); + inc(inc_context, new_bytes + i * element_width); } } - self.decref(alignment, element_width, elements_refcounted, dec, roc_ops); + self.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); return new_list; } @@ -314,21 +360,24 @@ pub const RocList = extern struct { new_length: usize, element_width: usize, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, roc_ops: *RocOps, ) RocList { if (self.bytes) |source_ptr| { - if (self.isUnique() and !self.isSeamlessSlice()) { + if (self.isUnique(roc_ops) and !self.isSeamlessSlice()) { const capacity = self.capacity_or_alloc_ptr; if (capacity >= new_length) { - return RocList{ .bytes = self.bytes, .length = new_length, .capacity_or_alloc_ptr = capacity }; + const result = RocList{ .bytes = self.bytes, .length = new_length, .capacity_or_alloc_ptr = capacity }; + return result; } else { const new_capacity = utils.calculateCapacity(capacity, new_length, element_width); - const new_source = utils.unsafeReallocate(source_ptr, alignment, capacity, new_capacity, element_width, elements_refcounted); - return RocList{ .bytes = new_source, .length = new_length, .capacity_or_alloc_ptr = new_capacity }; + const new_source = utils.unsafeReallocate(source_ptr, alignment, capacity, new_capacity, element_width, elements_refcounted, roc_ops); + const result = RocList{ .bytes = new_source, .length = new_length, .capacity_or_alloc_ptr = new_capacity }; + return result; } } - return self.reallocateFresh(alignment, new_length, element_width, elements_refcounted, inc, roc_ops); + return self.reallocateFresh(alignment, new_length, element_width, elements_refcounted, inc_context, inc, roc_ops); } return RocList.list_allocate(alignment, new_length, element_width, elements_refcounted, roc_ops); } @@ -340,6 +389,7 @@ pub const RocList = extern struct { new_length: usize, element_width: usize, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, roc_ops: *RocOps, ) RocList { @@ -358,21 +408,43 @@ pub const RocList = extern struct { if (elements_refcounted) { var i: usize = 0; while (i < old_length) : (i += 1) { - inc(dest_ptr + i * element_width); + inc(inc_context, dest_ptr + i * element_width); } } } // Calls utils.decref directly to avoid decrementing the refcount of elements. - utils.decref(self.getAllocationDataPtr(), self.capacity_or_alloc_ptr, alignment, elements_refcounted, roc_ops); + utils.decref(self.getAllocationDataPtr(roc_ops), self.capacity_or_alloc_ptr, alignment, elements_refcounted, roc_ops); return result; } }; /// Increment the reference count. -pub fn listIncref(list: RocList, amount: isize, elements_refcounted: bool) callconv(.C) void { - list.incref(amount, elements_refcounted); +pub fn listIncref(list: RocList, amount: isize, elements_refcounted: bool, roc_ops: *RocOps) callconv(.c) void { + list.incref(amount, elements_refcounted, roc_ops); +} + +/// Get the number of elements in the list. +pub fn listLen(list: RocList) callconv(.c) usize { + return list.len(); +} + +/// Check if the list is empty. +pub fn listIsEmpty(list: RocList) callconv(.c) bool { + return list.isEmpty(); +} + +/// Get a pointer to an element at the given index without bounds checking. +/// UNSAFE: No bounds checking is performed. Index must be < list.len(). +/// This is intended for internal use by low-level operations only. +/// Returns a pointer to the element at the given index. +pub fn listGetUnsafe(list: RocList, index: u64, element_width: usize) callconv(.c) ?[*]u8 { + if (list.bytes) |bytes| { + const byte_offset = @as(usize, @intCast(index)) * element_width; + return bytes + byte_offset; + } + return null; } /// Decrement reference count and deallocate when no longer shared. @@ -381,13 +453,15 @@ pub fn listDecref( alignment: u32, element_width: usize, elements_refcounted: bool, + dec_context: ?*anyopaque, dec: Dec, roc_ops: *RocOps, -) callconv(.C) void { +) callconv(.c) void { list.decref( alignment, element_width, elements_refcounted, + dec_context, dec, roc_ops, ); @@ -399,15 +473,17 @@ pub fn listWithCapacity( alignment: u32, element_width: usize, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { return listReserve( RocList.empty(), alignment, capacity, element_width, elements_refcounted, + inc_context, inc, .InPlace, roc_ops, @@ -421,15 +497,16 @@ pub fn listReserve( spare: u64, element_width: usize, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, update_mode: UpdateMode, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { const original_len = list.len(); const cap = @as(u64, @intCast(list.getCapacity())); const desired_cap = @as(u64, @intCast(original_len)) +| spare; - if ((update_mode == .InPlace or list.isUnique()) and cap >= desired_cap) { + if ((update_mode == .InPlace or list.isUnique(roc_ops)) and cap >= desired_cap) { return list; } else { // Make sure on 32-bit targets we don't accidentally wrap when we cast our U64 desired capacity to U32. @@ -440,6 +517,7 @@ pub fn listReserve( @as(usize, @intCast(reserve_size)), element_width, elements_refcounted, + inc_context, inc, roc_ops, ); @@ -454,17 +532,19 @@ pub fn listReleaseExcessCapacity( alignment: u32, element_width: usize, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, + dec_context: ?*anyopaque, dec: Dec, update_mode: UpdateMode, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { const old_length = list.len(); // We use the direct list.capacity_or_alloc_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_alloc_ptr == old_length) { + if ((update_mode == .InPlace or list.isUnique(roc_ops)) and list.capacity_or_alloc_ptr == old_length) { return list; } else if (old_length == 0) { - list.decref(alignment, element_width, elements_refcounted, dec, roc_ops); + list.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); return RocList.empty(); } else { // TODO: This can be made more efficient, but has to work around the `decref`. @@ -480,11 +560,11 @@ pub fn listReleaseExcessCapacity( var i: usize = 0; while (i < old_length) : (i += 1) { const element = source_ptr + i * element_width; - inc(element); + inc(inc_context, element); } } } - list.decref(alignment, element_width, elements_refcounted, dec, roc_ops); + list.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); return output; } } @@ -494,8 +574,8 @@ pub fn listAppendUnsafe( list: RocList, element: Opaque, element_width: usize, - copy: CopyFn, -) callconv(.C) RocList { + copy: CopyFallbackFn, +) callconv(.c) RocList { const old_length = list.len(); var output = list; output.length += 1; @@ -503,35 +583,46 @@ pub fn listAppendUnsafe( if (output.bytes) |bytes| { if (element) |source| { const target = bytes + old_length * element_width; - copy(target, source); + copy(target, source, element_width); } } return output; } -fn listAppend( +/// List.append - adds an element to the end of a list. +/// +/// ## Ownership +/// - `list`: **consumes** - caller loses ownership +/// - `element`: **borrows** - copied into list, caller retains original +/// - Returns: **copy-on-write** - may be same allocation if unique with capacity +/// +/// Reserves capacity if needed, then appends element. If the list is unique +/// with sufficient capacity, modifies in place and returns same pointer. +pub fn listAppend( list: RocList, alignment: u32, element: Opaque, element_width: usize, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, update_mode: UpdateMode, - copy: CopyFn, + copy_fn: CopyFallbackFn, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { const with_capacity = listReserve( list, alignment, 1, element_width, elements_refcounted, + inc_context, inc, update_mode, roc_ops, ); - return listAppendUnsafe(with_capacity, element, element_width, copy); + return listAppendUnsafe(with_capacity, element, element_width, copy_fn); } /// Directly mutate the given list to push an element onto the end, and then return it. @@ -555,7 +646,7 @@ pub fn pushInPlace( element_size: usize, element: *anyopaque, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { const old_length = list.len(); const old_capacity = list.getCapacity(); @@ -578,6 +669,7 @@ pub fn pushInPlace( new_length, element_size, false, + null, rcNone, roc_ops, ); @@ -610,7 +702,7 @@ pub fn shallowClone( elem_alignment: u32, elements_refcounted: bool, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { std.debug.assert(desired_capacity > 0); std.debug.assert(elem_size > 0); std.debug.assert(elem_alignment > 0); @@ -642,17 +734,27 @@ pub fn shallowClone( return new_list; } -/// Add element to beginning of list, shifting existing elements. +/// List.prepend - adds an element to the beginning of a list. +/// +/// ## Ownership +/// - `list`: **consumes** - caller loses ownership +/// - `element`: **borrows** - copied into list, caller retains original +/// - Returns: **copy-on-write** - may be same allocation if unique with capacity +/// +/// Reserves capacity if needed, shifts existing elements, then inserts element +/// at the front. If the list is unique with sufficient capacity, modifies in +/// place and returns same pointer. pub fn listPrepend( list: RocList, alignment: u32, element: Opaque, element_width: usize, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, copy: CopyFn, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { const old_length = list.len(); // TODO: properly wire in update mode. var with_capacity = listReserve( @@ -661,6 +763,7 @@ pub fn listPrepend( 1, element_width, elements_refcounted, + inc_context, inc, .Immutable, roc_ops, @@ -691,12 +794,14 @@ pub fn listSwap( index_1: u64, index_2: u64, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, + dec_context: ?*anyopaque, dec: Dec, update_mode: UpdateMode, copy: CopyFn, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { // Early exit to avoid swapping the same element. if (index_1 == index_2) return list; @@ -715,7 +820,9 @@ pub fn listSwap( alignment, element_width, elements_refcounted, + inc_context, inc, + dec_context, dec, roc_ops, ); @@ -732,7 +839,20 @@ pub fn listSwap( return newList; } -/// Returns a sublist of the given list +/// List.sublist - returns a sublist of the given list. +/// +/// ## Ownership +/// - `list`: **consumes** - caller loses ownership +/// - Returns: **copy-on-write** or **seamless-slice** depending on input +/// +/// If list is empty, or sublist range is empty/out-of-bounds: +/// - If unique: shrinks length to 0, returns same allocation +/// - Otherwise: decrefs original, returns empty list +/// +/// If sublist starts at index 0 and list is unique: +/// - Shrinks length in place, returns same allocation +/// +/// Otherwise: creates a seamless slice pointing into the original allocation. pub fn listSublist( list: RocList, alignment: u32, @@ -740,19 +860,20 @@ pub fn listSublist( elements_refcounted: bool, start_u64: u64, len_u64: u64, + dec_context: ?*anyopaque, dec: Dec, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { const size = list.len(); if (size == 0 or len_u64 == 0 or start_u64 >= @as(u64, @intCast(size))) { - if (list.isUnique()) { + if (list.isUnique(roc_ops)) { // Decrement the reference counts of all elements. if (list.bytes) |source_ptr| { if (elements_refcounted) { var i: usize = 0; while (i < size) : (i += 1) { const element = source_ptr + i * element_width; - dec(element); + dec(dec_context, element); } } } @@ -761,7 +882,7 @@ pub fn listSublist( output.length = 0; return output; } - list.decref(alignment, element_width, elements_refcounted, dec, roc_ops); + list.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); return RocList.empty(); } @@ -779,7 +900,7 @@ pub fn listSublist( // than something that fit in usize. const keep_len = @as(usize, @intCast(@min(len_u64, @as(u64, @intCast(size_minus_start))))); - if (start == 0 and list.isUnique()) { + if (start == 0 and list.isUnique(roc_ops)) { // The list is unique, we actually have to decrement refcounts to elements we aren't keeping around. // Decrement the reference counts of elements after `start + keep_len`. if (elements_refcounted) { @@ -787,7 +908,7 @@ pub fn listSublist( var i: usize = 0; while (i < drop_end_len) : (i += 1) { const element = source_ptr + (start + keep_len + i) * element_width; - dec(element); + dec(dec_context, element); } } @@ -795,13 +916,38 @@ pub fn listSublist( output.length = keep_len; return output; } else { - if (list.isUnique()) { - list.setAllocationElementCount(elements_refcounted); + if (list.isUnique(roc_ops)) { + // Store original element count for proper cleanup when the slice is freed. + // When the seamless slice is later decreffed, it will decref ALL elements + // starting from the original allocation pointer, not just the slice elements. + list.setAllocationElementCount(elements_refcounted, roc_ops); } const list_alloc_ptr = (@intFromPtr(source_ptr) >> 1) | SEAMLESS_SLICE_BIT; const slice_alloc_ptr = list.capacity_or_alloc_ptr; const slice_mask = list.seamlessSliceMask(); const alloc_ptr = (list_alloc_ptr & ~slice_mask) | (slice_alloc_ptr & slice_mask); + + // Verify the encoded pointer will decode correctly + if (comptime builtin.mode == .Debug) { + const test_decode = alloc_ptr << 1; + const original_ptr = if (list.isSeamlessSlice()) + slice_alloc_ptr << 1 + else + @intFromPtr(source_ptr); + if (test_decode != (original_ptr & ~@as(usize, 1))) { + var buf: [128]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, "listSublist: encoding error (test_decode=0x{x}, original_ptr=0x{x})", .{ test_decode, original_ptr }) catch "listSublist: encoding error"; + roc_ops.crash(msg); + } + + // Verify alignment of the original allocation pointer + if (original_ptr % @alignOf(usize) != 0) { + var buf: [128]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, "listSublist: misaligned original ptr=0x{x} (alignment={d})", .{ original_ptr, @alignOf(usize) }) catch "listSublist: misaligned original ptr"; + roc_ops.crash(msg); + } + } + return RocList{ .bytes = source_ptr + start * element_width, .length = keep_len, @@ -820,10 +966,12 @@ pub fn listDropAt( element_width: usize, elements_refcounted: bool, drop_index_u64: u64, + inc_context: ?*anyopaque, inc: Inc, + dec_context: ?*anyopaque, dec: Dec, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { const size = list.len(); const size_u64 = @as(u64, @intCast(size)); @@ -832,7 +980,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 <= 1) { - list.decref(alignment, element_width, elements_refcounted, dec, roc_ops); + list.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); return RocList.empty(); } @@ -847,6 +995,7 @@ pub fn listDropAt( elements_refcounted, 1, size -| 1, + dec_context, dec, roc_ops, ); @@ -860,6 +1009,7 @@ pub fn listDropAt( elements_refcounted, 0, size -| 1, + dec_context, dec, roc_ops, ); @@ -874,10 +1024,10 @@ pub fn listDropAt( // were >= than `size`, and we know `size` fits in usize. const drop_index: usize = @intCast(drop_index_u64); - if (list.isUnique()) { + if (list.isUnique(roc_ops)) { if (elements_refcounted) { const element = source_ptr + drop_index * element_width; - dec(element); + dec(dec_context, element); } const copy_target = source_ptr + (drop_index * element_width); @@ -912,11 +1062,11 @@ pub fn listDropAt( var i: usize = 0; while (i < output.len()) : (i += 1) { const cloned_elem = target_ptr + i * element_width; - inc(cloned_elem); + inc(inc_context, cloned_elem); } } - list.decref(alignment, element_width, elements_refcounted, dec, roc_ops); + list.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); return output; } else { @@ -929,16 +1079,19 @@ pub fn listSortWith( input: RocList, cmp: CompareFn, cmp_data: Opaque, + inc_n_context: ?*anyopaque, inc_n_data: IncN, data_is_owned: bool, alignment: u32, element_width: usize, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, + dec_context: ?*anyopaque, dec: Dec, copy: CopyFn, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { if (input.len() < 2) { return input; } @@ -946,7 +1099,9 @@ pub fn listSortWith( alignment, element_width, elements_refcounted, + inc_context, inc, + dec_context, dec, roc_ops, ); @@ -958,6 +1113,7 @@ pub fn listSortWith( cmp, cmp_data, data_is_owned, + inc_n_context, inc_n_data, element_width, alignment, @@ -1025,35 +1181,61 @@ fn swapElements( return swap(element_width, element_at_i, element_at_j, copy); } -/// Concatenates two lists into a new list containing all elements from both lists. +/// List.concat - concatenates two lists into one. /// -/// ## Ownership and Memory Management -/// **IMPORTANT**: This function CONSUMES both input lists (`list_a` and `list_b`). -/// The caller must NOT call `decref` on either input list after calling this function, -/// as this function handles their cleanup internally. +/// ## Ownership +/// - `list_a`: **consumes** - caller loses ownership +/// - `list_b`: **consumes** - caller loses ownership +/// - Returns: **independent** or **copy-on-write** - new allocation or extended list_a +/// +/// This function handles cleanup of both input lists internally. +/// If list_a has capacity, may extend it and return (copy-on-write). +/// Otherwise allocates new list containing elements from both. pub fn listConcat( list_a: RocList, list_b: RocList, alignment: u32, element_width: usize, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, + dec_context: ?*anyopaque, dec: Dec, roc_ops: *RocOps, -) callconv(.C) RocList { - // NOTE we always use list_a! because it is owned, we must consume it, and it may have unused capacity - if (list_b.isEmpty()) { - if (list_a.getCapacity() == 0) { - // a could be a seamless slice, so we still need to decref. - list_a.decref(alignment, element_width, elements_refcounted, dec, roc_ops); - return list_b; - } else { - // we must consume this list. Even though it has no elements, it could still have capacity - list_b.decref(alignment, element_width, elements_refcounted, dec, roc_ops); - +) callconv(.c) RocList { + // Early return for empty lists - avoid unnecessary allocations + if (list_a.isEmpty()) { + if (list_b.isEmpty()) { + // Both are empty, return list_a and clean up list_b + list_b.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); return list_a; + } else { + // list_a is empty, list_b has elements - return list_b + // list_a might still need decref if it has capacity + list_a.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); + return list_b; } - } else if (list_a.isUnique()) { + } else if (list_b.isEmpty()) { + // list_b is empty, list_a has elements - return list_a + // list_b might still need decref if it has capacity + list_b.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); + return list_a; + } + + // Check if both lists share the same underlying allocation. + // This can happen when the same list is passed as both arguments (e.g., in repeat_helper). + const same_allocation = blk: { + const alloc_a = list_a.getAllocationDataPtr(roc_ops); + const alloc_b = list_b.getAllocationDataPtr(roc_ops); + break :blk (alloc_a != null and alloc_a == alloc_b); + }; + + // If they share the same allocation, we must: + // 1. NOT use the unique paths (reallocate might free/move the allocation) + // 2. Only decref once at the end (to avoid double-free) + // Instead, fall through to the general path that allocates a new list. + + if (!same_allocation and list_a.isUnique(roc_ops)) { const total_length: usize = list_a.len() + list_b.len(); const resized_list_a = list_a.reallocate( @@ -1061,6 +1243,7 @@ pub fn listConcat( total_length, element_width, elements_refcounted, + inc_context, inc, roc_ops, ); @@ -1068,22 +1251,26 @@ pub fn listConcat( // These must exist, otherwise, the lists would have been empty. const source_a = resized_list_a.bytes orelse unreachable; const source_b = list_b.bytes orelse unreachable; - @memcpy(source_a[(list_a.len() * element_width)..(total_length * element_width)], source_b[0..(list_b.len() * element_width)]); + + // Use @memmove instead of @memcpy to handle potential aliasing + const dest_slice = source_a[(list_a.len() * element_width)..(total_length * element_width)]; + const src_slice = source_b[0..(list_b.len() * element_width)]; + @memmove(dest_slice, src_slice); // Increment refcount of all cloned elements. if (elements_refcounted) { var i: usize = 0; while (i < list_b.len()) : (i += 1) { const cloned_elem = source_b + i * element_width; - inc(cloned_elem); + inc(inc_context, cloned_elem); } } // decrement list b. - list_b.decref(alignment, element_width, elements_refcounted, dec, roc_ops); + list_b.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); return resized_list_a; - } else if (list_b.isUnique()) { + } else if (!same_allocation and list_b.isUnique(roc_ops)) { const total_length: usize = list_a.len() + list_b.len(); const resized_list_b = list_b.reallocate( @@ -1091,6 +1278,7 @@ pub fn listConcat( total_length, element_width, elements_refcounted, + inc_context, inc, roc_ops, ); @@ -1112,12 +1300,12 @@ pub fn listConcat( var i: usize = 0; while (i < list_a.len()) : (i += 1) { const cloned_elem = source_a + i * element_width; - inc(cloned_elem); + inc(inc_context, cloned_elem); } } // decrement list a. - list_a.decref(alignment, element_width, elements_refcounted, dec, roc_ops); + list_a.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); return resized_list_b; } @@ -1138,18 +1326,21 @@ pub fn listConcat( var i: usize = 0; while (i < list_a.len()) : (i += 1) { const cloned_elem = source_a + i * element_width; - inc(cloned_elem); + inc(inc_context, cloned_elem); } i = 0; while (i < list_b.len()) : (i += 1) { const cloned_elem = source_b + i * element_width; - inc(cloned_elem); + inc(inc_context, cloned_elem); } } // decrement list a and b. - list_a.decref(alignment, element_width, elements_refcounted, dec, roc_ops); - list_b.decref(alignment, element_width, elements_refcounted, dec, roc_ops); + // If they share the same allocation, only decref once to avoid double-free. + list_a.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); + if (!same_allocation) { + list_b.decref(alignment, element_width, elements_refcounted, dec_context, dec, roc_ops); + } return output; } @@ -1162,7 +1353,7 @@ pub fn listReplaceInPlace( element_width: usize, out_element: ?[*]u8, copy: CopyFn, -) callconv(.C) RocList { +) callconv(.c) RocList { // INVARIANT: bounds checking happens on the roc side // // at the time of writing, the function is implemented roughly as @@ -1181,12 +1372,14 @@ pub fn listReplace( element: Opaque, element_width: usize, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, + dec_context: ?*anyopaque, dec: Dec, out_element: ?[*]u8, copy: CopyFn, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { // INVARIANT: bounds checking happens on the roc side // // at the time of writing, the function is implemented roughly as @@ -1196,7 +1389,7 @@ pub fn listReplace( // and it's always safe to cast index to usize. // because inserting into an empty list is always out of bounds return listReplaceInPlaceHelp( - list.makeUnique(alignment, element_width, elements_refcounted, inc, dec, roc_ops), + list.makeUnique(alignment, element_width, elements_refcounted, inc_context, inc, dec_context, dec, roc_ops), @as(usize, @intCast(index)), element, element_width, @@ -1228,8 +1421,9 @@ inline fn listReplaceInPlaceHelp( /// Check if list has exclusive ownership for safe in-place modification. pub fn listIsUnique( list: RocList, -) callconv(.C) bool { - return list.isEmpty() or list.isUnique(); + roc_ops: *RocOps, +) callconv(.c) bool { + return list.isEmpty() or list.isUnique(roc_ops); } /// Create independent copy for safe mutation when list is shared. @@ -1238,42 +1432,46 @@ pub fn listClone( alignment: u32, element_width: usize, elements_refcounted: bool, + inc_context: ?*anyopaque, inc: Inc, + dec_context: ?*anyopaque, dec: Dec, roc_ops: *RocOps, -) callconv(.C) RocList { - return list.makeUnique(alignment, element_width, elements_refcounted, inc, dec, roc_ops); +) callconv(.c) RocList { + return list.makeUnique(alignment, element_width, elements_refcounted, inc_context, inc, dec_context, dec, roc_ops); } /// Get current allocated capacity for growth planning. pub fn listCapacity( list: RocList, -) callconv(.C) usize { +) callconv(.c) usize { return list.getCapacity(); } /// Get raw memory pointer for direct access patterns. pub fn listAllocationPtr( list: RocList, -) callconv(.C) ?[*]u8 { - return list.getAllocationDataPtr(); + roc_ops: *RocOps, +) callconv(.c) ?[*]u8 { + return list.getAllocationDataPtr(roc_ops); } -fn rcNone(_: ?[*]u8) callconv(.C) void {} +/// No-op reference counting function for non-refcounted types +pub fn rcNone(_: ?*anyopaque, _: ?[*]u8) callconv(.c) void {} /// Append UTF-8 string bytes to list for efficient string-to-bytes conversion. pub fn listConcatUtf8( list: RocList, string: RocStr, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { if (string.len() == 0) { return list; } else { const combined_length = list.len() + string.len(); // List U8 has alignment 1 and element_width 1 - const result = list.reallocate(1, combined_length, 1, false, &rcNone, roc_ops); + const result = list.reallocate(1, combined_length, 1, false, null, &rcNone, roc_ops); // We just allocated combined_length, which is > 0 because string.len() > 0 var bytes = result.bytes orelse unreachable; @memcpy(bytes[list.len()..combined_length], string.asU8ptr()[0..string.len()]); @@ -1282,24 +1480,134 @@ pub fn listConcatUtf8( } } +/// Specialized copy fn which takes pointers as pointers to U8 and copies from src to dest. +pub fn copy_u8(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *u8 = utils.alignedPtrCast(*u8, dest.?, @src()); + const src_ptr: *const u8 = utils.alignedPtrCast(*const u8, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to I8 and copies from src to dest. +pub fn copy_i8(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *i8 = utils.alignedPtrCast(*i8, dest.?, @src()); + const src_ptr: *const i8 = utils.alignedPtrCast(*const i8, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to U16 and copies from src to dest. +pub fn copy_u16(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *u16 = utils.alignedPtrCast(*u16, dest.?, @src()); + const src_ptr: *const u16 = utils.alignedPtrCast(*const u16, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to I16 and copies from src to dest. +pub fn copy_i16(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *i16 = utils.alignedPtrCast(*i16, dest.?, @src()); + const src_ptr: *const i16 = utils.alignedPtrCast(*const i16, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to U32 and copies from src to dest. +pub fn copy_u32(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *u32 = utils.alignedPtrCast(*u32, dest.?, @src()); + const src_ptr: *const u32 = utils.alignedPtrCast(*const u32, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to I32 and copies from src to dest. +pub fn copy_i32(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *i32 = utils.alignedPtrCast(*i32, dest.?, @src()); + const src_ptr: *const i32 = utils.alignedPtrCast(*const i32, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to U64 and copies from src to dest. +pub fn copy_u64(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *u64 = utils.alignedPtrCast(*u64, dest.?, @src()); + const src_ptr: *const u64 = utils.alignedPtrCast(*const u64, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to I64 and copies from src to dest. +pub fn copy_i64(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *i64 = utils.alignedPtrCast(*i64, dest.?, @src()); + const src_ptr: *const i64 = utils.alignedPtrCast(*const i64, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to U128 and copies from src to dest. +pub fn copy_u128(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *u128 = utils.alignedPtrCast(*u128, dest.?, @src()); + const src_ptr: *const u128 = utils.alignedPtrCast(*const u128, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to I128 and copies from src to dest. +pub fn copy_i128(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *i128 = utils.alignedPtrCast(*i128, dest.?, @src()); + const src_ptr: *const i128 = utils.alignedPtrCast(*const i128, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to Boxes and copies from src to dest. +pub fn copy_box(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *usize = utils.alignedPtrCast(*usize, dest.?, @src()); + const src_ptr: *const usize = utils.alignedPtrCast(*const usize, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to ZST Boxes and copies from src to dest. +pub fn copy_box_zst(dest: Opaque, _: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *usize = utils.alignedPtrCast(*usize, dest.?, @src()); + dest_ptr.* = 0; +} + +/// Specialized copy fn which takes pointers as pointers to Lists and copies from src to dest. +pub fn copy_list(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *RocList = utils.alignedPtrCast(*RocList, dest.?, @src()); + const src_ptr: *const RocList = utils.alignedPtrCast(*const RocList, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to ZST Lists and copies from src to dest. +pub fn copy_list_zst(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *RocList = utils.alignedPtrCast(*RocList, dest.?, @src()); + const src_ptr: *const RocList = utils.alignedPtrCast(*const RocList, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to a RocStr and copies from src to dest. +pub fn copy_str(dest: Opaque, src: Opaque, _: usize) callconv(.c) void { + const dest_ptr: *RocStr = utils.alignedPtrCast(*RocStr, dest.?, @src()); + const src_ptr: *const RocStr = utils.alignedPtrCast(*const RocStr, src.?, @src()); + dest_ptr.* = src_ptr.*; +} + +/// Specialized copy fn which takes pointers as pointers to u8 and copies from src to dest. +pub fn copy_fallback(dest: Opaque, source: Opaque, width: usize) callconv(.c) void { + const src: []u8 = source.?[0..width]; + const dst: []u8 = dest.?[0..width]; + @memmove(dst, src); +} + test "listConcat: non-unique with unique overlapping" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); const nonUnique = RocList.fromSlice(u8, ([_]u8{1})[0..], false, test_env.getOps()); const bytes: [*]u8 = @as([*]u8, @ptrCast(nonUnique.bytes)); - const ptr_width = @sizeOf(usize); - const refcount_ptr = @as([*]isize, @ptrCast(@as([*]align(ptr_width) u8, @alignCast(bytes)) - ptr_width)); - utils.increfRcPtrC(&refcount_ptr[0], 1); + const refcount_ptr: [*]isize = utils.alignedPtrCast([*]isize, bytes - @sizeOf(usize), @src()); + utils.increfRcPtrC(&refcount_ptr[0], 1, test_env.getOps()); // NOTE: nonUnique will be consumed by listConcat, so no defer decref needed const unique = RocList.fromSlice(u8, ([_]u8{ 2, 3, 4 })[0..], false, test_env.getOps()); // NOTE: unique will be consumed by listConcat, so no defer decref needed - var concatted = listConcat(nonUnique, unique, 1, 1, false, rcNone, rcNone, test_env.getOps()); - defer concatted.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + var concatted = listConcat(nonUnique, unique, 1, 1, false, null, rcNone, null, rcNone, test_env.getOps()); + defer concatted.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); var wanted = RocList.fromSlice(u8, ([_]u8{ 1, 2, 3, 4 })[0..], false, test_env.getOps()); - defer wanted.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer wanted.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expect(concatted.eql(wanted)); } @@ -1314,9 +1622,9 @@ test "listConcatUtf8" { const string = RocStr.init(string_bytes.ptr, string_bytes.len, test_env.getOps()); defer string.decref(test_env.getOps()); const ret = listConcatUtf8(list, string, test_env.getOps()); - defer ret.decref(1, 1, false, &rcNone, test_env.getOps()); + defer ret.decref(1, 1, false, null, &rcNone, test_env.getOps()); const expected = RocList.fromSlice(u8, &[_]u8{ 1, 2, 3, 4, 240, 159, 144, 166 }, false, test_env.getOps()); - defer expected.decref(1, 1, false, &rcNone, test_env.getOps()); + defer expected.decref(1, 1, false, null, &rcNone, test_env.getOps()); try std.testing.expect(ret.eql(expected)); } @@ -1325,7 +1633,7 @@ test "RocList empty list creation" { defer test_env.deinit(); const empty_list = RocList.empty(); - defer empty_list.decref(1, 1, false, rcNone, test_env.getOps()); + defer empty_list.decref(1, 1, false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 0), empty_list.len()); try std.testing.expect(empty_list.isEmpty()); @@ -1337,7 +1645,7 @@ test "RocList fromSlice basic functionality" { const data = [_]i32{ 10, 20, 30, 40 }; const list = RocList.fromSlice(i32, data[0..], false, test_env.getOps()); - defer list.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 4), list.len()); try std.testing.expect(!list.isEmpty()); @@ -1349,7 +1657,7 @@ test "RocList elements access" { const data = [_]u8{ 1, 2, 3, 4, 5 }; const list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); const elements_ptr = list.elements(u8); try std.testing.expect(elements_ptr != null); @@ -1367,7 +1675,7 @@ test "RocList capacity operations" { const data = [_]i16{ 100, 200 }; const list = RocList.fromSlice(i16, data[0..], false, test_env.getOps()); - defer list.decref(@alignOf(i16), @sizeOf(i16), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(i16), @sizeOf(i16), false, null, rcNone, test_env.getOps()); const capacity = list.getCapacity(); try std.testing.expect(capacity >= list.len()); @@ -1383,13 +1691,13 @@ test "RocList equality operations" { const data3 = [_]u8{ 1, 2, 4 }; const list1 = RocList.fromSlice(u8, data1[0..], false, test_env.getOps()); - defer list1.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list1.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); const list2 = RocList.fromSlice(u8, data2[0..], false, test_env.getOps()); - defer list2.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list2.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); const list3 = RocList.fromSlice(u8, data3[0..], false, test_env.getOps()); - defer list3.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list3.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Equal lists should be equal try std.testing.expect(list1.eql(list2)); @@ -1401,9 +1709,9 @@ test "RocList equality operations" { // Empty lists should be equal const empty1 = RocList.empty(); - defer empty1.decref(1, 1, false, rcNone, test_env.getOps()); + defer empty1.decref(1, 1, false, null, rcNone, test_env.getOps()); const empty2 = RocList.empty(); - defer empty2.decref(1, 1, false, rcNone, test_env.getOps()); + defer empty2.decref(1, 1, false, null, rcNone, test_env.getOps()); try std.testing.expect(empty1.eql(empty2)); } @@ -1415,16 +1723,16 @@ test "RocList uniqueness and cloning" { const list = RocList.fromSlice(i32, data[0..], false, test_env.getOps()); // A freshly created list should be unique - try std.testing.expect(list.isUnique()); + try std.testing.expect(list.isUnique(test_env.getOps())); // Make the list non-unique by incrementing reference count - list.incref(1, false); - defer list.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); - try std.testing.expect(!list.isUnique()); + list.incref(1, false, test_env.getOps()); + defer list.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); + try std.testing.expect(!list.isUnique(test_env.getOps())); // Clone the list (this will consume one reference to the original) - const cloned = listClone(list, @alignOf(i32), @sizeOf(i32), false, rcNone, rcNone, test_env.getOps()); - defer cloned.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + const cloned = listClone(list, @alignOf(i32), @sizeOf(i32), false, null, rcNone, null, rcNone, test_env.getOps()); + defer cloned.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); // Both should be equal but different objects (since list was not unique) try std.testing.expect(list.eql(cloned)); @@ -1437,17 +1745,17 @@ test "RocList isUnique with reference counting" { const data = [_]u8{ 1, 2, 3 }; const list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Should be unique initially - try std.testing.expect(list.isUnique()); + try std.testing.expect(list.isUnique(test_env.getOps())); // Increment reference count - list.incref(1, false); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + list.incref(1, false, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Should no longer be unique - try std.testing.expect(!list.isUnique()); + try std.testing.expect(!list.isUnique(test_env.getOps())); } test "listWithCapacity basic functionality" { @@ -1455,8 +1763,8 @@ test "listWithCapacity basic functionality" { defer test_env.deinit(); const capacity: usize = 10; - const list = listWithCapacity(capacity, @alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); - defer list.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + const list = listWithCapacity(capacity, @alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); + defer list.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); // Should have the requested capacity try std.testing.expect(list.getCapacity() >= capacity); @@ -1472,8 +1780,8 @@ test "listReserve functionality" { const data = [_]u8{ 1, 2, 3 }; const list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); - const reserved_list = listReserve(list, @alignOf(u8), 20, @sizeOf(u8), false, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); - defer reserved_list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const reserved_list = listReserve(list, @alignOf(u8), 20, @sizeOf(u8), false, null, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); + defer reserved_list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Should have at least the requested capacity try std.testing.expect(reserved_list.getCapacity() >= 20); @@ -1494,7 +1802,7 @@ test "listCapacity function" { const data = [_]i16{ 100, 200, 300 }; const list = RocList.fromSlice(i16, data[0..], false, test_env.getOps()); - defer list.decref(@alignOf(i16), @sizeOf(i16), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(i16), @sizeOf(i16), false, null, rcNone, test_env.getOps()); const capacity = listCapacity(list); try std.testing.expectEqual(list.getCapacity(), capacity); @@ -1507,7 +1815,7 @@ test "RocList allocateExact functionality" { const exact_size: usize = 5; const list = RocList.allocateExact(@alignOf(u64), exact_size, @sizeOf(u64), false, test_env.getOps()); - defer list.decref(@alignOf(u64), @sizeOf(u64), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u64), @sizeOf(u64), false, null, rcNone, test_env.getOps()); // Should have exactly the requested capacity (or very close) try std.testing.expectEqual(exact_size, list.getCapacity()); @@ -1525,15 +1833,15 @@ test "listReleaseExcessCapacity functionality" { const list_with_data = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); // Reserve excess capacity for it - const list_with_excess = listReserve(list_with_data, @alignOf(u8), 100, @sizeOf(u8), false, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); + const list_with_excess = listReserve(list_with_data, @alignOf(u8), 100, @sizeOf(u8), false, null, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); // Verify it has excess capacity try std.testing.expect(list_with_excess.getCapacity() >= 100); try std.testing.expectEqual(@as(usize, 3), list_with_excess.len()); // Release the excess capacity - const released_list = listReleaseExcessCapacity(list_with_excess, @alignOf(u8), @sizeOf(u8), false, rcNone, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); - defer released_list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const released_list = listReleaseExcessCapacity(list_with_excess, @alignOf(u8), @sizeOf(u8), false, null, rcNone, null, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); + defer released_list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // The released list should have capacity close to its length and preserve the data try std.testing.expectEqual(@as(usize, 3), released_list.len()); @@ -1558,8 +1866,8 @@ test "listSublist basic functionality" { // Note: listSublist consumes the original list // Extract middle portion - const sublist = listSublist(list, @alignOf(u8), @sizeOf(u8), false, 2, 4, rcNone, test_env.getOps()); - defer sublist.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const sublist = listSublist(list, @alignOf(u8), @sizeOf(u8), false, 2, 4, null, rcNone, test_env.getOps()); + defer sublist.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 4), sublist.len()); @@ -1580,8 +1888,8 @@ test "listSublist edge cases" { const list = RocList.fromSlice(i32, data[0..], false, test_env.getOps()); // Take empty sublist - const empty_sublist = listSublist(list, @alignOf(i32), @sizeOf(i32), false, 1, 0, rcNone, test_env.getOps()); - defer empty_sublist.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + const empty_sublist = listSublist(list, @alignOf(i32), @sizeOf(i32), false, 1, 0, null, rcNone, test_env.getOps()); + defer empty_sublist.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 0), empty_sublist.len()); try std.testing.expect(empty_sublist.isEmpty()); @@ -1597,7 +1905,7 @@ test "listSwap basic functionality" { // Swap elements at indices 1 and 3 // Proper copy function for u16 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u16, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u16, @ptrCast(@alignCast(src))); @@ -1606,8 +1914,8 @@ test "listSwap basic functionality" { } }.copy; - const swapped_list = listSwap(list, @alignOf(u16), @sizeOf(u16), 1, 3, false, rcNone, rcNone, utils.UpdateMode.Immutable, copy_fn, test_env.getOps()); - defer swapped_list.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + const swapped_list = listSwap(list, @alignOf(u16), @sizeOf(u16), 1, 3, false, null, rcNone, null, rcNone, utils.UpdateMode.Immutable, copy_fn, test_env.getOps()); + defer swapped_list.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 4), swapped_list.len()); @@ -1625,28 +1933,17 @@ test "listAppendUnsafe basic functionality" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); - // Copy function for u8 elements - const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { - if (dest != null and src != null) { - const dest_ptr = @as(*u8, @ptrCast(@alignCast(dest))); - const src_ptr = @as(*u8, @ptrCast(@alignCast(src))); - dest_ptr.* = src_ptr.*; - } - } - }.copy; - // Create a list with some capacity - var list = listWithCapacity(10, @alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + var list = listWithCapacity(10, @alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Add some initial elements using listAppendUnsafe const element1: u8 = 42; - list = listAppendUnsafe(list, @as(?[*]u8, @ptrCast(@constCast(&element1))), @sizeOf(u8), copy_fn); + list = listAppendUnsafe(list, @as(?[*]u8, @ptrCast(@constCast(&element1))), @sizeOf(u8), ©_fallback); const element2: u8 = 84; - list = listAppendUnsafe(list, @as(?[*]u8, @ptrCast(@constCast(&element2))), @sizeOf(u8), copy_fn); + list = listAppendUnsafe(list, @as(?[*]u8, @ptrCast(@constCast(&element2))), @sizeOf(u8), ©_fallback); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 2), list.len()); @@ -1661,24 +1958,13 @@ test "listAppendUnsafe with different types" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); - // Copy function for i32 elements - const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { - if (dest != null and src != null) { - const dest_ptr = @as(*i32, @ptrCast(@alignCast(dest))); - const src_ptr = @as(*i32, @ptrCast(@alignCast(src))); - dest_ptr.* = src_ptr.*; - } - } - }.copy; - // Test with i32 - var int_list = listWithCapacity(5, @alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + var int_list = listWithCapacity(5, @alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); const int_val: i32 = -123; - int_list = listAppendUnsafe(int_list, @as(?[*]u8, @ptrCast(@constCast(&int_val))), @sizeOf(i32), copy_fn); + int_list = listAppendUnsafe(int_list, @as(?[*]u8, @ptrCast(@constCast(&int_val))), @sizeOf(i32), ©_fallback); - defer int_list.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + defer int_list.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 1), int_list.len()); @@ -1692,24 +1978,13 @@ test "listAppendUnsafe with pre-allocated capacity" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); - // Copy function for u16 elements - const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { - if (dest != null and src != null) { - const dest_ptr = @as(*u16, @ptrCast(@alignCast(dest))); - const src_ptr = @as(*u16, @ptrCast(@alignCast(src))); - dest_ptr.* = src_ptr.*; - } - } - }.copy; - // Create a list with capacity (listAppendUnsafe requires pre-allocated space) - var list_with_capacity = listWithCapacity(5, @alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + var list_with_capacity = listWithCapacity(5, @alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); const element: u16 = 9999; - list_with_capacity = listAppendUnsafe(list_with_capacity, @as(?[*]u8, @ptrCast(@constCast(&element))), @sizeOf(u16), copy_fn); + list_with_capacity = listAppendUnsafe(list_with_capacity, @as(?[*]u8, @ptrCast(@constCast(&element))), @sizeOf(u16), ©_fallback); - defer list_with_capacity.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + defer list_with_capacity.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 1), list_with_capacity.len()); try std.testing.expect(!list_with_capacity.isEmpty()); @@ -1726,7 +2001,7 @@ test "listPrepend basic functionality" { // Copy function for u8 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u8, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u8, @ptrCast(@alignCast(src))); @@ -1741,8 +2016,8 @@ test "listPrepend basic functionality" { // Prepend an element const element: u8 = 1; - const result = listPrepend(list, @alignOf(u8), @as(?[*]u8, @ptrCast(@constCast(&element))), @sizeOf(u8), false, rcNone, copy_fn, test_env.getOps()); - defer result.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const result = listPrepend(list, @alignOf(u8), @as(?[*]u8, @ptrCast(@constCast(&element))), @sizeOf(u8), false, null, rcNone, copy_fn, test_env.getOps()); + defer result.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 4), result.len()); @@ -1761,7 +2036,7 @@ test "listPrepend to empty list" { // Copy function for i32 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*i32, @ptrCast(@alignCast(dest))); const src_ptr = @as(*i32, @ptrCast(@alignCast(src))); @@ -1775,8 +2050,8 @@ test "listPrepend to empty list" { // Prepend an element const element: i32 = 42; - const result = listPrepend(empty_list, @alignOf(i32), @as(?[*]u8, @ptrCast(@constCast(&element))), @sizeOf(i32), false, rcNone, copy_fn, test_env.getOps()); - defer result.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + const result = listPrepend(empty_list, @alignOf(i32), @as(?[*]u8, @ptrCast(@constCast(&element))), @sizeOf(i32), false, null, rcNone, copy_fn, test_env.getOps()); + defer result.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 1), result.len()); try std.testing.expect(!result.isEmpty()); @@ -1793,7 +2068,7 @@ test "listPrepend multiple elements" { // Copy function for u16 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u16, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u16, @ptrCast(@alignCast(src))); @@ -1808,13 +2083,13 @@ test "listPrepend multiple elements" { // Prepend first element const element1: u16 = 200; - list = listPrepend(list, @alignOf(u16), @as(?[*]u8, @ptrCast(@constCast(&element1))), @sizeOf(u16), false, rcNone, copy_fn, test_env.getOps()); + list = listPrepend(list, @alignOf(u16), @as(?[*]u8, @ptrCast(@constCast(&element1))), @sizeOf(u16), false, null, rcNone, copy_fn, test_env.getOps()); // Prepend second element const element2: u16 = 300; - list = listPrepend(list, @alignOf(u16), @as(?[*]u8, @ptrCast(@constCast(&element2))), @sizeOf(u16), false, rcNone, copy_fn, test_env.getOps()); + list = listPrepend(list, @alignOf(u16), @as(?[*]u8, @ptrCast(@constCast(&element2))), @sizeOf(u16), false, null, rcNone, copy_fn, test_env.getOps()); - defer list.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 3), list.len()); @@ -1835,8 +2110,8 @@ test "listDropAt basic functionality" { const list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); // Drop element at index 2 (value 30) - const result = listDropAt(list, @alignOf(u8), @sizeOf(u8), false, 2, rcNone, rcNone, test_env.getOps()); - defer result.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const result = listDropAt(list, @alignOf(u8), @sizeOf(u8), false, 2, null, rcNone, null, rcNone, test_env.getOps()); + defer result.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 4), result.len()); @@ -1858,8 +2133,8 @@ test "listDropAt first element" { const list = RocList.fromSlice(i32, data[0..], false, test_env.getOps()); // Drop first element (index 0) - const result = listDropAt(list, @alignOf(i32), @sizeOf(i32), false, 0, rcNone, rcNone, test_env.getOps()); - defer result.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + const result = listDropAt(list, @alignOf(i32), @sizeOf(i32), false, 0, null, rcNone, null, rcNone, test_env.getOps()); + defer result.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 2), result.len()); @@ -1879,8 +2154,8 @@ test "listDropAt last element" { const list = RocList.fromSlice(u16, data[0..], false, test_env.getOps()); // Drop last element (index 3) - const result = listDropAt(list, @alignOf(u16), @sizeOf(u16), false, 3, rcNone, rcNone, test_env.getOps()); - defer result.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + const result = listDropAt(list, @alignOf(u16), @sizeOf(u16), false, 3, null, rcNone, null, rcNone, test_env.getOps()); + defer result.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 3), result.len()); @@ -1901,8 +2176,8 @@ test "listDropAt single element list" { const list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); // Drop the only element (index 0) - const result = listDropAt(list, @alignOf(u8), @sizeOf(u8), false, 0, rcNone, rcNone, test_env.getOps()); - defer result.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const result = listDropAt(list, @alignOf(u8), @sizeOf(u8), false, 0, null, rcNone, null, rcNone, test_env.getOps()); + defer result.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 0), result.len()); try std.testing.expect(result.isEmpty()); @@ -1917,8 +2192,8 @@ test "listDropAt out of bounds" { const list = RocList.fromSlice(i16, data[0..], false, test_env.getOps()); // Try to drop at index 5 (out of bounds) - const result = listDropAt(list, @alignOf(i16), @sizeOf(i16), false, 5, rcNone, rcNone, test_env.getOps()); - defer result.decref(@alignOf(i16), @sizeOf(i16), false, rcNone, test_env.getOps()); + const result = listDropAt(list, @alignOf(i16), @sizeOf(i16), false, 5, null, rcNone, null, rcNone, test_env.getOps()); + defer result.decref(@alignOf(i16), @sizeOf(i16), false, null, rcNone, test_env.getOps()); // Should return the original list unchanged try std.testing.expectEqual(@as(usize, 3), result.len()); @@ -1937,7 +2212,7 @@ test "listReplace basic functionality" { // Copy function for u8 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u8, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u8, @ptrCast(@alignCast(src))); @@ -1953,8 +2228,8 @@ test "listReplace basic functionality" { // Replace element at index 2 (value 30) with 99 const new_element: u8 = 99; var out_element: u8 = 0; - const result = listReplace(list, @alignOf(u8), 2, @as(?[*]u8, @ptrCast(@constCast(&new_element))), @sizeOf(u8), false, rcNone, rcNone, @as(?[*]u8, @ptrCast(&out_element)), copy_fn, test_env.getOps()); - defer result.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const result = listReplace(list, @alignOf(u8), 2, @as(?[*]u8, @ptrCast(@constCast(&new_element))), @sizeOf(u8), false, null, rcNone, null, rcNone, @as(?[*]u8, @ptrCast(&out_element)), copy_fn, test_env.getOps()); + defer result.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 4), result.len()); try std.testing.expectEqual(@as(u8, 30), out_element); // original value @@ -1974,7 +2249,7 @@ test "listReplace first element" { // Copy function for i32 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*i32, @ptrCast(@alignCast(dest))); const src_ptr = @as(*i32, @ptrCast(@alignCast(src))); @@ -1990,8 +2265,8 @@ test "listReplace first element" { // Replace first element (index 0) const new_element: i32 = -999; var out_element: i32 = 0; - const result = listReplace(list, @alignOf(i32), 0, @as(?[*]u8, @ptrCast(@constCast(&new_element))), @sizeOf(i32), false, rcNone, rcNone, @as(?[*]u8, @ptrCast(&out_element)), copy_fn, test_env.getOps()); - defer result.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + const result = listReplace(list, @alignOf(i32), 0, @as(?[*]u8, @ptrCast(@constCast(&new_element))), @sizeOf(i32), false, null, rcNone, null, rcNone, @as(?[*]u8, @ptrCast(&out_element)), copy_fn, test_env.getOps()); + defer result.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 3), result.len()); try std.testing.expectEqual(@as(i32, 100), out_element); // original value @@ -2010,7 +2285,7 @@ test "listReplace last element" { // Copy function for u16 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u16, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u16, @ptrCast(@alignCast(src))); @@ -2026,8 +2301,8 @@ test "listReplace last element" { // Replace last element (index 3) const new_element: u16 = 9999; var out_element: u16 = 0; - const result = listReplace(list, @alignOf(u16), 3, @as(?[*]u8, @ptrCast(@constCast(&new_element))), @sizeOf(u16), false, rcNone, rcNone, @as(?[*]u8, @ptrCast(&out_element)), copy_fn, test_env.getOps()); - defer result.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + const result = listReplace(list, @alignOf(u16), 3, @as(?[*]u8, @ptrCast(@constCast(&new_element))), @sizeOf(u16), false, null, rcNone, null, rcNone, @as(?[*]u8, @ptrCast(&out_element)), copy_fn, test_env.getOps()); + defer result.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 4), result.len()); try std.testing.expectEqual(@as(u16, 4), out_element); // original value @@ -2047,7 +2322,7 @@ test "listReplace single element list" { // Copy function for u8 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u8, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u8, @ptrCast(@alignCast(src))); @@ -2063,8 +2338,8 @@ test "listReplace single element list" { // Replace the only element (index 0) const new_element: u8 = 84; var out_element: u8 = 0; - const result = listReplace(list, @alignOf(u8), 0, @as(?[*]u8, @ptrCast(@constCast(&new_element))), @sizeOf(u8), false, rcNone, rcNone, @as(?[*]u8, @ptrCast(&out_element)), copy_fn, test_env.getOps()); - defer result.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const result = listReplace(list, @alignOf(u8), 0, @as(?[*]u8, @ptrCast(@constCast(&new_element))), @sizeOf(u8), false, null, rcNone, null, rcNone, @as(?[*]u8, @ptrCast(&out_element)), copy_fn, test_env.getOps()); + defer result.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 1), result.len()); try std.testing.expectEqual(@as(u8, 42), out_element); // original value @@ -2082,8 +2357,8 @@ test "edge case: listConcat with empty lists" { const empty1 = RocList.empty(); const empty2 = RocList.empty(); - const result = listConcat(empty1, empty2, 1, 1, false, rcNone, rcNone, test_env.getOps()); - defer result.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const result = listConcat(empty1, empty2, 1, 1, false, null, rcNone, null, rcNone, test_env.getOps()); + defer result.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 0), result.len()); try std.testing.expect(result.isEmpty()); @@ -2098,16 +2373,16 @@ test "edge case: listConcat one empty one non-empty" { const non_empty = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); // Empty + non-empty - const result1 = listConcat(empty_list, non_empty, 1, 1, false, rcNone, rcNone, test_env.getOps()); - defer result1.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const result1 = listConcat(empty_list, non_empty, 1, 1, false, null, rcNone, null, rcNone, test_env.getOps()); + defer result1.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 3), result1.len()); // Non-empty + empty const empty2 = RocList.empty(); const non_empty2 = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); - const result2 = listConcat(non_empty2, empty2, 1, 1, false, rcNone, rcNone, test_env.getOps()); - defer result2.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const result2 = listConcat(non_empty2, empty2, 1, 1, false, null, rcNone, null, rcNone, test_env.getOps()); + defer result2.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 3), result2.len()); } @@ -2120,8 +2395,8 @@ test "edge case: listSublist with zero length" { const list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); // Extract zero-length sublist from middle - const sublist = listSublist(list, @alignOf(u8), @sizeOf(u8), false, 2, 0, rcNone, test_env.getOps()); - defer sublist.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const sublist = listSublist(list, @alignOf(u8), @sizeOf(u8), false, 2, 0, null, rcNone, test_env.getOps()); + defer sublist.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 0), sublist.len()); try std.testing.expect(sublist.isEmpty()); @@ -2135,8 +2410,8 @@ test "edge case: listSublist entire list" { const list = RocList.fromSlice(i16, data[0..], false, test_env.getOps()); // Extract entire list as sublist - const sublist = listSublist(list, @alignOf(i16), @sizeOf(i16), false, 0, 3, rcNone, test_env.getOps()); - defer sublist.decref(@alignOf(i16), @sizeOf(i16), false, rcNone, test_env.getOps()); + const sublist = listSublist(list, @alignOf(i16), @sizeOf(i16), false, 0, 3, null, rcNone, test_env.getOps()); + defer sublist.decref(@alignOf(i16), @sizeOf(i16), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 3), sublist.len()); @@ -2154,7 +2429,7 @@ test "edge case: listPrepend to large list" { // Copy function for u8 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u8, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u8, @ptrCast(@alignCast(src))); @@ -2172,8 +2447,8 @@ test "edge case: listPrepend to large list" { // Prepend an element const element: u8 = 255; - const result = listPrepend(list, @alignOf(u8), @as(?[*]u8, @ptrCast(@constCast(&element))), @sizeOf(u8), false, rcNone, copy_fn, test_env.getOps()); - defer result.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const result = listPrepend(list, @alignOf(u8), @as(?[*]u8, @ptrCast(@constCast(&element))), @sizeOf(u8), false, null, rcNone, copy_fn, test_env.getOps()); + defer result.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 101), result.len()); @@ -2189,8 +2464,8 @@ test "edge case: listWithCapacity zero capacity" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); - const list = listWithCapacity(0, @alignOf(u32), @sizeOf(u32), false, rcNone, test_env.getOps()); - defer list.decref(@alignOf(u32), @sizeOf(u32), false, rcNone, test_env.getOps()); + const list = listWithCapacity(0, @alignOf(u32), @sizeOf(u32), false, null, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u32), @sizeOf(u32), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 0), list.len()); try std.testing.expect(list.isEmpty()); @@ -2203,10 +2478,10 @@ test "edge case: RocList equality with different capacities" { // Create two lists with same content but different capacities const data = [_]u8{ 1, 2, 3 }; const list1 = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); - defer list1.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list1.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Create list with larger capacity - var list2 = listWithCapacity(10, @alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + var list2 = listWithCapacity(10, @alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Manually set the same content list2.length = 3; if (list2.bytes) |bytes| { @@ -2214,7 +2489,7 @@ test "edge case: RocList equality with different capacities" { bytes[i] = val; } } - defer list2.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list2.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Should be equal despite different capacities try std.testing.expect(list1.eql(list2)); @@ -2225,31 +2500,20 @@ test "edge case: listAppendUnsafe multiple times" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); - // Copy function for u8 elements - const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { - if (dest != null and src != null) { - const dest_ptr = @as(*u8, @ptrCast(@alignCast(dest))); - const src_ptr = @as(*u8, @ptrCast(@alignCast(src))); - dest_ptr.* = src_ptr.*; - } - } - }.copy; - // Create a list with sufficient capacity - var list = listWithCapacity(5, @alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + var list = listWithCapacity(5, @alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Append multiple elements const element1: u8 = 10; - list = listAppendUnsafe(list, @as(?[*]u8, @ptrCast(@constCast(&element1))), @sizeOf(u8), copy_fn); + list = listAppendUnsafe(list, @as(?[*]u8, @ptrCast(@constCast(&element1))), @sizeOf(u8), ©_fallback); const element2: u8 = 20; - list = listAppendUnsafe(list, @as(?[*]u8, @ptrCast(@constCast(&element2))), @sizeOf(u8), copy_fn); + list = listAppendUnsafe(list, @as(?[*]u8, @ptrCast(@constCast(&element2))), @sizeOf(u8), ©_fallback); const element3: u8 = 30; - list = listAppendUnsafe(list, @as(?[*]u8, @ptrCast(@constCast(&element3))), @sizeOf(u8), copy_fn); + list = listAppendUnsafe(list, @as(?[*]u8, @ptrCast(@constCast(&element3))), @sizeOf(u8), ©_fallback); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 3), list.len()); @@ -2268,13 +2532,13 @@ test "seamless slice: isSeamlessSlice detection" { // Regular list should not be a seamless slice const data = [_]u8{ 1, 2, 3 }; const regular_list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); - defer regular_list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer regular_list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expect(!regular_list.isSeamlessSlice()); // Empty list should not be a seamless slice const empty_list = RocList.empty(); - defer empty_list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer empty_list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expect(!empty_list.isSeamlessSlice()); } @@ -2286,13 +2550,13 @@ test "seamless slice: seamlessSliceMask functionality" { // Regular list should have mask of all zeros const data = [_]u8{ 1, 2, 3 }; const regular_list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); - defer regular_list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer regular_list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 0), regular_list.seamlessSliceMask()); // Empty list should have mask of all zeros const empty_list = RocList.empty(); - defer empty_list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer empty_list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 0), empty_list.seamlessSliceMask()); } @@ -2316,7 +2580,7 @@ test "seamless slice: getCapacity behavior" { // Regular list capacity const data = [_]u8{ 1, 2, 3 }; const regular_list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); - defer regular_list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer regular_list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); const regular_capacity = regular_list.getCapacity(); try std.testing.expect(regular_capacity >= 3); @@ -2340,26 +2604,26 @@ test "complex reference counting: multiple increfs and decrefs" { const list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); // Should be unique initially - try std.testing.expect(list.isUnique()); + try std.testing.expect(list.isUnique(test_env.getOps())); // Increment reference count multiple times - list.incref(1, false); - list.incref(1, false); - list.incref(1, false); + list.incref(1, false, test_env.getOps()); + list.incref(1, false, test_env.getOps()); + list.incref(1, false, test_env.getOps()); // Should no longer be unique - try std.testing.expect(!list.isUnique()); + try std.testing.expect(!list.isUnique(test_env.getOps())); // Decrement back down - list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); - list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); - list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); + list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); + list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Should be unique again - try std.testing.expect(list.isUnique()); + try std.testing.expect(list.isUnique(test_env.getOps())); // Final cleanup - list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); } test "complex reference counting: makeUnique with shared list" { @@ -2370,18 +2634,18 @@ test "complex reference counting: makeUnique with shared list" { const original_list = RocList.fromSlice(i32, data[0..], false, test_env.getOps()); // Make the list non-unique by incrementing reference count - original_list.incref(1, false); - defer original_list.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + original_list.incref(1, false, test_env.getOps()); + defer original_list.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); - try std.testing.expect(!original_list.isUnique()); + try std.testing.expect(!original_list.isUnique(test_env.getOps())); // makeUnique should create a new copy - const unique_list = original_list.makeUnique(@alignOf(i32), @sizeOf(i32), false, rcNone, rcNone, test_env.getOps()); - defer unique_list.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + const unique_list = original_list.makeUnique(@alignOf(i32), @sizeOf(i32), false, null, rcNone, null, rcNone, test_env.getOps()); + defer unique_list.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); // The unique list should be different from the original try std.testing.expect(unique_list.bytes != original_list.bytes); - try std.testing.expect(unique_list.isUnique()); + try std.testing.expect(unique_list.isUnique(test_env.getOps())); // But should have the same content try std.testing.expect(unique_list.eql(original_list)); @@ -2395,17 +2659,17 @@ test "complex reference counting: listIsUnique consistency" { const list = RocList.fromSlice(u16, data[0..], false, test_env.getOps()); // Test that listIsUnique function matches isUnique method - try std.testing.expectEqual(list.isUnique(), listIsUnique(list)); + try std.testing.expectEqual(list.isUnique(test_env.getOps()), listIsUnique(list, test_env.getOps())); // After incref, both should report not unique - list.incref(1, false); - defer list.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + list.incref(1, false, test_env.getOps()); + defer list.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); - try std.testing.expectEqual(list.isUnique(), listIsUnique(list)); - try std.testing.expect(!listIsUnique(list)); + try std.testing.expectEqual(list.isUnique(test_env.getOps()), listIsUnique(list, test_env.getOps())); + try std.testing.expect(!listIsUnique(list, test_env.getOps())); // Final cleanup - list.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + list.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); } test "complex reference counting: clone behavior" { @@ -2416,11 +2680,11 @@ test "complex reference counting: clone behavior" { const original_list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); // Clone should create a new independent copy - const cloned_list = listClone(original_list, @alignOf(u8), @sizeOf(u8), false, rcNone, rcNone, test_env.getOps()); - defer cloned_list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const cloned_list = listClone(original_list, @alignOf(u8), @sizeOf(u8), false, null, rcNone, null, rcNone, test_env.getOps()); + defer cloned_list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Cloned list should be unique and have same content - try std.testing.expect(cloned_list.isUnique()); + try std.testing.expect(cloned_list.isUnique(test_env.getOps())); try std.testing.expect(cloned_list.eql(original_list)); } @@ -2429,10 +2693,10 @@ test "complex reference counting: empty list operations" { defer test_env.deinit(); const empty_list = RocList.empty(); - defer empty_list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer empty_list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Empty lists should handle basic operations gracefully - try std.testing.expect(empty_list.isUnique()); + try std.testing.expect(empty_list.isUnique(test_env.getOps())); try std.testing.expect(empty_list.isEmpty()); try std.testing.expectEqual(@as(usize, 0), empty_list.len()); } @@ -2443,7 +2707,7 @@ test "listReplaceInPlace basic functionality" { // Copy function for u8 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u8, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u8, @ptrCast(@alignCast(src))); @@ -2460,7 +2724,7 @@ test "listReplaceInPlace basic functionality" { const new_element: u8 = 99; var out_element: u8 = 0; const result = listReplaceInPlace(list, 2, @as(?[*]u8, @ptrCast(@constCast(&new_element))), @sizeOf(u8), @as(?[*]u8, @ptrCast(&out_element)), copy_fn); - defer result.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer result.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 4), result.len()); try std.testing.expectEqual(@as(u8, 30), out_element); // original value @@ -2480,7 +2744,7 @@ test "listReplaceInPlace first and last elements" { // Copy function for i32 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*i32, @ptrCast(@alignCast(dest))); const src_ptr = @as(*i32, @ptrCast(@alignCast(src))); @@ -2496,7 +2760,7 @@ test "listReplaceInPlace first and last elements" { const new_first: i32 = -999; var out_first: i32 = 0; const result1 = listReplaceInPlace(list1, 0, @as(?[*]u8, @ptrCast(@constCast(&new_first))), @sizeOf(i32), @as(?[*]u8, @ptrCast(&out_first)), copy_fn); - defer result1.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + defer result1.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(i32, 100), out_first); const elements1_ptr = result1.elements(i32); @@ -2510,7 +2774,7 @@ test "listReplaceInPlace first and last elements" { const new_last: i32 = 999; var out_last: i32 = 0; const result2 = listReplaceInPlace(list2, 2, @as(?[*]u8, @ptrCast(@constCast(&new_last))), @sizeOf(i32), @as(?[*]u8, @ptrCast(&out_last)), copy_fn); - defer result2.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + defer result2.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(i32, 300), out_last); const elements2_ptr = result2.elements(i32); @@ -2525,7 +2789,7 @@ test "listReplaceInPlace single element list" { // Copy function for u16 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u16, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u16, @ptrCast(@alignCast(src))); @@ -2542,7 +2806,7 @@ test "listReplaceInPlace single element list" { const new_element: u16 = 84; var out_element: u16 = 0; const result = listReplaceInPlace(list, 0, @as(?[*]u8, @ptrCast(@constCast(&new_element))), @sizeOf(u16), @as(?[*]u8, @ptrCast(&out_element)), copy_fn); - defer result.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + defer result.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 1), result.len()); try std.testing.expectEqual(@as(u16, 42), out_element); // original value @@ -2559,7 +2823,7 @@ test "listReplaceInPlace vs listReplace comparison" { // Copy function for u8 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u8, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u8, @ptrCast(@alignCast(src))); @@ -2575,14 +2839,14 @@ test "listReplaceInPlace vs listReplace comparison" { const new_element1: u8 = 99; var out_element1: u8 = 0; const result1 = listReplaceInPlace(list1, 2, @as(?[*]u8, @ptrCast(@constCast(&new_element1))), @sizeOf(u8), @as(?[*]u8, @ptrCast(&out_element1)), copy_fn); - defer result1.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer result1.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Test listReplace with same parameters const list2 = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); const new_element2: u8 = 99; var out_element2: u8 = 0; - const result2 = listReplace(list2, @alignOf(u8), 2, @as(?[*]u8, @ptrCast(@constCast(&new_element2))), @sizeOf(u8), false, rcNone, rcNone, @as(?[*]u8, @ptrCast(&out_element2)), copy_fn, test_env.getOps()); - defer result2.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const result2 = listReplace(list2, @alignOf(u8), 2, @as(?[*]u8, @ptrCast(@constCast(&new_element2))), @sizeOf(u8), false, null, rcNone, null, rcNone, @as(?[*]u8, @ptrCast(&out_element2)), copy_fn, test_env.getOps()); + defer result2.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Both should produce the same result try std.testing.expect(result1.eql(result2)); @@ -2596,9 +2860,9 @@ test "listAllocationPtr basic functionality" { // Test with regular list const data = [_]u8{ 1, 2, 3, 4 }; const list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); - const alloc_ptr = listAllocationPtr(list); + const alloc_ptr = listAllocationPtr(list, test_env.getOps()); try std.testing.expect(alloc_ptr != null); // The allocation pointer should be valid and accessible @@ -2613,9 +2877,9 @@ test "listAllocationPtr empty list" { defer test_env.deinit(); const empty_list = RocList.empty(); - defer empty_list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer empty_list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); - const alloc_ptr = listAllocationPtr(empty_list); + const alloc_ptr = listAllocationPtr(empty_list, test_env.getOps()); // Empty lists may have null allocation pointer _ = alloc_ptr; // Just verify the function doesn't crash } @@ -2628,22 +2892,22 @@ test "listIncref and listDecref public functions" { const list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); // Should be unique initially - try std.testing.expect(list.isUnique()); + try std.testing.expect(list.isUnique(test_env.getOps())); // Use public listIncref function - listIncref(list, 1, false); + listIncref(list, 1, false, test_env.getOps()); // Should no longer be unique - try std.testing.expect(!list.isUnique()); + try std.testing.expect(!list.isUnique(test_env.getOps())); // Use public listDecref function - listDecref(list, @alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + listDecref(list, @alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Should be unique again - try std.testing.expect(list.isUnique()); + try std.testing.expect(list.isUnique(test_env.getOps())); // Final cleanup - listDecref(list, @alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + listDecref(list, @alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); } test "integration: prepend then drop operations" { @@ -2652,7 +2916,7 @@ test "integration: prepend then drop operations" { // Copy function for u8 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u8, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u8, @ptrCast(@alignCast(src))); @@ -2667,21 +2931,21 @@ test "integration: prepend then drop operations" { // Prepend multiple elements const element1: u8 = 1; - list = listPrepend(list, @alignOf(u8), @as(?[*]u8, @ptrCast(@constCast(&element1))), @sizeOf(u8), false, rcNone, copy_fn, test_env.getOps()); + list = listPrepend(list, @alignOf(u8), @as(?[*]u8, @ptrCast(@constCast(&element1))), @sizeOf(u8), false, null, rcNone, copy_fn, test_env.getOps()); const element2: u8 = 2; - list = listPrepend(list, @alignOf(u8), @as(?[*]u8, @ptrCast(@constCast(&element2))), @sizeOf(u8), false, rcNone, copy_fn, test_env.getOps()); + list = listPrepend(list, @alignOf(u8), @as(?[*]u8, @ptrCast(@constCast(&element2))), @sizeOf(u8), false, null, rcNone, copy_fn, test_env.getOps()); // Now we should have [2, 1, 5, 10, 15] try std.testing.expectEqual(@as(usize, 5), list.len()); // Drop the middle element (index 2, value 5) - list = listDropAt(list, @alignOf(u8), @sizeOf(u8), false, 2, rcNone, rcNone, test_env.getOps()); + list = listDropAt(list, @alignOf(u8), @sizeOf(u8), false, 2, null, rcNone, null, rcNone, test_env.getOps()); // Now we should have [2, 1, 10, 15] try std.testing.expectEqual(@as(usize, 4), list.len()); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); const elements_ptr = list.elements(u8); try std.testing.expect(elements_ptr != null); @@ -2704,14 +2968,14 @@ test "integration: concat then sublist operations" { const list2 = RocList.fromSlice(i16, data2[0..], false, test_env.getOps()); // Concatenate them - const concatenated = listConcat(list1, list2, @alignOf(i16), @sizeOf(i16), false, rcNone, rcNone, test_env.getOps()); + const concatenated = listConcat(list1, list2, @alignOf(i16), @sizeOf(i16), false, null, rcNone, null, rcNone, test_env.getOps()); // Should have [100, 200, 300, 400, 500] try std.testing.expectEqual(@as(usize, 5), concatenated.len()); // Extract a sublist from the middle - const sublist = listSublist(concatenated, @alignOf(i16), @sizeOf(i16), false, 1, 3, rcNone, test_env.getOps()); - defer sublist.decref(@alignOf(i16), @sizeOf(i16), false, rcNone, test_env.getOps()); + const sublist = listSublist(concatenated, @alignOf(i16), @sizeOf(i16), false, 1, 3, null, rcNone, test_env.getOps()); + defer sublist.decref(@alignOf(i16), @sizeOf(i16), false, null, rcNone, test_env.getOps()); // Should have [200, 300, 400] try std.testing.expectEqual(@as(usize, 3), sublist.len()); @@ -2730,7 +2994,7 @@ test "integration: replace then swap operations" { // Copy function for u32 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u32, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u32, @ptrCast(@alignCast(src))); @@ -2746,15 +3010,15 @@ test "integration: replace then swap operations" { // Replace element at index 1 (20 -> 99) const new_element: u32 = 99; var out_element: u32 = 0; - list = listReplace(list, @alignOf(u32), 1, @as(?[*]u8, @ptrCast(@constCast(&new_element))), @sizeOf(u32), false, rcNone, rcNone, @as(?[*]u8, @ptrCast(&out_element)), copy_fn, test_env.getOps()); + list = listReplace(list, @alignOf(u32), 1, @as(?[*]u8, @ptrCast(@constCast(&new_element))), @sizeOf(u32), false, null, rcNone, null, rcNone, @as(?[*]u8, @ptrCast(&out_element)), copy_fn, test_env.getOps()); try std.testing.expectEqual(@as(u32, 20), out_element); // Now we should have [10, 99, 30, 40] // Swap elements at indices 0 and 2 (10 <-> 30) - list = listSwap(list, @alignOf(u32), @sizeOf(u32), 0, 2, false, rcNone, rcNone, utils.UpdateMode.Immutable, copy_fn, test_env.getOps()); + list = listSwap(list, @alignOf(u32), @sizeOf(u32), 0, 2, false, null, rcNone, null, rcNone, utils.UpdateMode.Immutable, copy_fn, test_env.getOps()); - defer list.decref(@alignOf(u32), @sizeOf(u32), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u32), @sizeOf(u32), false, null, rcNone, test_env.getOps()); // Now we should have [30, 99, 10, 40] try std.testing.expectEqual(@as(usize, 4), list.len()); @@ -2786,8 +3050,8 @@ test "stress: large list operations" { try std.testing.expect(large_list.getCapacity() >= large_size); // Test sublist on large list (note: listSublist consumes the original list) - const mid_sublist = listSublist(large_list, @alignOf(u16), @sizeOf(u16), false, 400, 200, rcNone, test_env.getOps()); - defer mid_sublist.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + const mid_sublist = listSublist(large_list, @alignOf(u16), @sizeOf(u16), false, 400, 200, null, rcNone, test_env.getOps()); + defer mid_sublist.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 200), mid_sublist.len()); @@ -2803,24 +3067,13 @@ test "stress: many small operations" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); - // Copy function for u8 elements - const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { - if (dest != null and src != null) { - const dest_ptr = @as(*u8, @ptrCast(@alignCast(dest))); - const src_ptr = @as(*u8, @ptrCast(@alignCast(src))); - dest_ptr.* = src_ptr.*; - } - } - }.copy; - // Start with a list with some capacity - var list = listWithCapacity(50, @alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + var list = listWithCapacity(50, @alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Add many elements using listAppendUnsafe var i: u8 = 0; while (i < 20) : (i += 1) { - list = listAppendUnsafe(list, @as(?[*]u8, @ptrCast(@constCast(&i))), @sizeOf(u8), copy_fn); + list = listAppendUnsafe(list, @as(?[*]u8, @ptrCast(@constCast(&i))), @sizeOf(u8), ©_fallback); } try std.testing.expectEqual(@as(usize, 20), list.len()); @@ -2834,7 +3087,7 @@ test "stress: many small operations" { try std.testing.expectEqual(@as(u8, @intCast(idx)), elem); } - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); } test "memory management: capacity boundary conditions" { @@ -2843,14 +3096,14 @@ test "memory management: capacity boundary conditions" { // Create a list with exact capacity const exact_capacity: usize = 10; - var list = listWithCapacity(exact_capacity, @alignOf(u32), @sizeOf(u32), false, rcNone, test_env.getOps()); + var list = listWithCapacity(exact_capacity, @alignOf(u32), @sizeOf(u32), false, null, rcNone, test_env.getOps()); try std.testing.expect(list.getCapacity() >= exact_capacity); try std.testing.expectEqual(@as(usize, 0), list.len()); // Use listReserve to ensure we have exactly the capacity we want - list = listReserve(list, @alignOf(u32), exact_capacity, @sizeOf(u32), false, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); - defer list.decref(@alignOf(u32), @sizeOf(u32), false, rcNone, test_env.getOps()); + list = listReserve(list, @alignOf(u32), exact_capacity, @sizeOf(u32), false, null, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); + defer list.decref(@alignOf(u32), @sizeOf(u32), false, null, rcNone, test_env.getOps()); // Verify capacity management functions work correctly const initial_capacity = list.getCapacity(); @@ -2869,14 +3122,14 @@ test "memory management: release excess capacity edge cases" { const small_list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); // Reserve much more capacity than needed - const oversized_list = listReserve(small_list, @alignOf(u8), 1000, @sizeOf(u8), false, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); + const oversized_list = listReserve(small_list, @alignOf(u8), 1000, @sizeOf(u8), false, null, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); try std.testing.expectEqual(@as(usize, 1), oversized_list.len()); try std.testing.expect(oversized_list.getCapacity() >= 1000); // Release excess capacity - const trimmed_list = listReleaseExcessCapacity(oversized_list, @alignOf(u8), @sizeOf(u8), false, rcNone, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); - defer trimmed_list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const trimmed_list = listReleaseExcessCapacity(oversized_list, @alignOf(u8), @sizeOf(u8), false, null, rcNone, null, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); + defer trimmed_list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Should maintain content but reduce capacity try std.testing.expectEqual(@as(usize, 1), trimmed_list.len()); @@ -2899,20 +3152,20 @@ test "boundary conditions: zero-sized operations" { // Zero-length sublist from start const list1 = RocList.fromSlice(u16, data[0..], false, test_env.getOps()); - const empty_start = listSublist(list1, @alignOf(u16), @sizeOf(u16), false, 0, 0, rcNone, test_env.getOps()); - defer empty_start.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + const empty_start = listSublist(list1, @alignOf(u16), @sizeOf(u16), false, 0, 0, null, rcNone, test_env.getOps()); + defer empty_start.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 0), empty_start.len()); // Zero-length sublist from middle const list2 = RocList.fromSlice(u16, data[0..], false, test_env.getOps()); - const empty_mid = listSublist(list2, @alignOf(u16), @sizeOf(u16), false, 2, 0, rcNone, test_env.getOps()); - defer empty_mid.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + const empty_mid = listSublist(list2, @alignOf(u16), @sizeOf(u16), false, 2, 0, null, rcNone, test_env.getOps()); + defer empty_mid.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 0), empty_mid.len()); // Zero-length sublist from end const list3 = RocList.fromSlice(u16, data[0..], false, test_env.getOps()); - const empty_end = listSublist(list3, @alignOf(u16), @sizeOf(u16), false, 5, 0, rcNone, test_env.getOps()); - defer empty_end.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + const empty_end = listSublist(list3, @alignOf(u16), @sizeOf(u16), false, 5, 0, null, rcNone, test_env.getOps()); + defer empty_end.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 0), empty_end.len()); } @@ -2924,8 +3177,8 @@ test "boundary conditions: maximum index operations" { // Test dropAt with index at boundary (last valid index) const list1 = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); - const dropped_last = listDropAt(list1, @alignOf(u8), @sizeOf(u8), false, 2, rcNone, rcNone, test_env.getOps()); - defer dropped_last.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const dropped_last = listDropAt(list1, @alignOf(u8), @sizeOf(u8), false, 2, null, rcNone, null, rcNone, test_env.getOps()); + defer dropped_last.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 2), dropped_last.len()); const elements_ptr = dropped_last.elements(u8); @@ -2936,8 +3189,8 @@ test "boundary conditions: maximum index operations" { // Test dropAt with out-of-bounds index (should return original list) const list2 = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); - const dropped_oob = listDropAt(list2, @alignOf(u8), @sizeOf(u8), false, 10, rcNone, rcNone, test_env.getOps()); - defer dropped_oob.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const dropped_oob = listDropAt(list2, @alignOf(u8), @sizeOf(u8), false, 10, null, rcNone, null, rcNone, test_env.getOps()); + defer dropped_oob.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); try std.testing.expectEqual(@as(usize, 3), dropped_oob.len()); } @@ -2950,16 +3203,16 @@ test "memory management: clone with different update modes" { const original = RocList.fromSlice(i32, data[0..], false, test_env.getOps()); // Make the list non-unique - original.incref(1, false); - defer original.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + original.incref(1, false, test_env.getOps()); + defer original.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); // Clone should create an independent copy - const cloned = listClone(original, @alignOf(i32), @sizeOf(i32), false, rcNone, rcNone, test_env.getOps()); - defer cloned.decref(@alignOf(i32), @sizeOf(i32), false, rcNone, test_env.getOps()); + const cloned = listClone(original, @alignOf(i32), @sizeOf(i32), false, null, rcNone, null, rcNone, test_env.getOps()); + defer cloned.decref(@alignOf(i32), @sizeOf(i32), false, null, rcNone, test_env.getOps()); // Verify independence - they should have the same content but different memory try std.testing.expect(cloned.eql(original)); - try std.testing.expect(cloned.isUnique()); + try std.testing.expect(cloned.isUnique(test_env.getOps())); // Verify they can be modified independently by testing capacity operations const cloned_capacity = cloned.getCapacity(); @@ -2978,7 +3231,7 @@ test "boundary conditions: swap with identical indices" { // Copy function for u8 elements const copy_fn = struct { - fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.C) void { + fn copy(dest: ?[*]u8, src: ?[*]u8) callconv(.c) void { if (dest != null and src != null) { const dest_ptr = @as(*u8, @ptrCast(@alignCast(dest))); const src_ptr = @as(*u8, @ptrCast(@alignCast(src))); @@ -2991,8 +3244,8 @@ test "boundary conditions: swap with identical indices" { const list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); // Swap element with itself (index 2 with index 2) - const swapped = listSwap(list, @alignOf(u8), @sizeOf(u8), 2, 2, false, rcNone, rcNone, utils.UpdateMode.Immutable, copy_fn, test_env.getOps()); - defer swapped.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const swapped = listSwap(list, @alignOf(u8), @sizeOf(u8), 2, 2, false, null, rcNone, null, rcNone, utils.UpdateMode.Immutable, copy_fn, test_env.getOps()); + defer swapped.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Should be unchanged try std.testing.expectEqual(@as(usize, 4), swapped.len()); @@ -3014,17 +3267,17 @@ test "memory management: multiple reserve operations" { var list = RocList.fromSlice(u8, data[0..], false, test_env.getOps()); // Reserve capacity multiple times, each time increasing - list = listReserve(list, @alignOf(u8), 10, @sizeOf(u8), false, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); + list = listReserve(list, @alignOf(u8), 10, @sizeOf(u8), false, null, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); try std.testing.expect(list.getCapacity() >= 12); // 2 existing + 10 spare - list = listReserve(list, @alignOf(u8), 20, @sizeOf(u8), false, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); + list = listReserve(list, @alignOf(u8), 20, @sizeOf(u8), false, null, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); try std.testing.expect(list.getCapacity() >= 22); // 2 existing + 20 spare - list = listReserve(list, @alignOf(u8), 5, @sizeOf(u8), false, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); + list = listReserve(list, @alignOf(u8), 5, @sizeOf(u8), false, null, rcNone, utils.UpdateMode.Immutable, test_env.getOps()); // Should not decrease capacity, so still >= 22 try std.testing.expect(list.getCapacity() >= 22); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); // Verify content is preserved through all operations try std.testing.expectEqual(@as(usize, 2), list.len()); @@ -3054,7 +3307,7 @@ test "push: basic functionality with empty list" { try std.testing.expect(elements_ptr != null); try std.testing.expectEqual(@as(u8, 42), elements_ptr.?[0]); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); } test "push: multiple elements with reallocation" { @@ -3081,7 +3334,7 @@ test "push: multiple elements with reallocation" { try std.testing.expectEqual(expected, elements[i]); } - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); } test "push: with pre-existing capacity" { @@ -3089,7 +3342,7 @@ test "push: with pre-existing capacity" { defer test_env.deinit(); // Create a list with capacity but no elements - var list = listWithCapacity(10, @alignOf(u32), @sizeOf(u32), false, rcNone, test_env.getOps()); + var list = listWithCapacity(10, @alignOf(u32), @sizeOf(u32), false, null, rcNone, test_env.getOps()); const initial_capacity = list.getCapacity(); try std.testing.expect(initial_capacity >= 10); @@ -3112,7 +3365,7 @@ test "push: with pre-existing capacity" { try std.testing.expectEqual(@as(u32, 100), elements[0]); try std.testing.expectEqual(@as(u32, 200), elements[1]); - defer list.decref(@alignOf(u32), @sizeOf(u32), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u32), @sizeOf(u32), false, null, rcNone, test_env.getOps()); } test "push: different sized elements" { @@ -3136,7 +3389,7 @@ test "push: different sized elements" { try std.testing.expectEqual(@as(u64, 0xDEADBEEF), elements[0]); try std.testing.expectEqual(@as(u64, 0xCAFEBABE), elements[1]); - defer list.decref(@alignOf(u64), @sizeOf(u64), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u64), @sizeOf(u64), false, null, rcNone, test_env.getOps()); } test "push: stress test with many elements" { @@ -3165,7 +3418,7 @@ test "push: stress test with many elements" { try std.testing.expectEqual(i, elements[i]); } - defer list.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); } test "append: with unique list (refcount 1)" { @@ -3179,7 +3432,7 @@ test "append: with unique list (refcount 1)" { list = pushInPlace(list, @alignOf(u8), @sizeOf(u8), @ptrCast(@constCast(&value1)), test_env.getOps()); // Verify it's unique - try std.testing.expectEqual(@as(usize, 1), list.refcount()); + try std.testing.expectEqual(@as(usize, 1), list.refcount(test_env.getOps())); // Append should mutate in place const value2: u8 = 20; @@ -3193,7 +3446,7 @@ test "append: with unique list (refcount 1)" { try std.testing.expectEqual(@as(u8, 10), elements[0]); try std.testing.expectEqual(@as(u8, 20), elements[1]); - defer result.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer result.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); } test "append: with shared list (refcount > 1)" { @@ -3208,8 +3461,8 @@ test "append: with shared list (refcount > 1)" { list = pushInPlace(list, @alignOf(u8), @sizeOf(u8), @ptrCast(@constCast(&value2)), test_env.getOps()); // Increment refcount to simulate sharing - list.incref(1, false); - try std.testing.expect(list.refcount() > 1); + list.incref(1, false, test_env.getOps()); + try std.testing.expect(list.refcount(test_env.getOps()) > 1); // Append should clone const value3: u8 = 50; @@ -3225,8 +3478,8 @@ test "append: with shared list (refcount > 1)" { try std.testing.expectEqual(@as(u8, 200), result_elements[1]); try std.testing.expectEqual(@as(u8, 50), result_elements[2]); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); - defer result.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); + defer result.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); } test "append: with empty list" { @@ -3244,7 +3497,7 @@ test "append: with empty list" { try std.testing.expect(elements_ptr != null); try std.testing.expectEqual(@as(u32, 42), elements_ptr.?[0]); - defer result.decref(@alignOf(u32), @sizeOf(u32), false, rcNone, test_env.getOps()); + defer result.decref(@alignOf(u32), @sizeOf(u32), false, null, rcNone, test_env.getOps()); } test "push: large element types" { @@ -3278,7 +3531,7 @@ test "push: large element types" { try std.testing.expectEqual(@as(u64, 5), elements[1].a); try std.testing.expectEqual(@as(u64, 6), elements[1].b); - defer list.decref(@alignOf(LargeElement), @sizeOf(LargeElement), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(LargeElement), @sizeOf(LargeElement), false, null, rcNone, test_env.getOps()); } test "push: with exact capacity boundary" { @@ -3286,7 +3539,7 @@ test "push: with exact capacity boundary" { defer test_env.deinit(); // Create list with exact capacity of 3 - var list = listWithCapacity(3, @alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + var list = listWithCapacity(3, @alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); const initial_capacity = list.getCapacity(); try std.testing.expect(initial_capacity >= 3); @@ -3317,7 +3570,7 @@ test "push: with exact capacity boundary" { try std.testing.expectEqual(@as(u16, 333), elements[2]); try std.testing.expectEqual(@as(u16, 444), elements[3]); - defer list.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); } test "push: single byte elements" { @@ -3344,7 +3597,7 @@ test "push: single byte elements" { try std.testing.expectEqual(@as(u8, 'l'), elements[3]); try std.testing.expectEqual(@as(u8, 'o'), elements[4]); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); } test "append: refcount transitions" { @@ -3356,7 +3609,7 @@ test "append: refcount transitions" { const val1: u8 = 10; list = pushInPlace(list, @alignOf(u8), @sizeOf(u8), @ptrCast(@constCast(&val1)), test_env.getOps()); - try std.testing.expectEqual(@as(usize, 1), list.refcount()); + try std.testing.expectEqual(@as(usize, 1), list.refcount(test_env.getOps())); // Append while unique (should use push) const val2: u8 = 20; @@ -3365,8 +3618,8 @@ test "append: refcount transitions" { try std.testing.expectEqual(@as(usize, 2), result1.len()); // Increment refcount - result1.incref(1, false); - try std.testing.expect(result1.refcount() > 1); + result1.incref(1, false, test_env.getOps()); + try std.testing.expect(result1.refcount(test_env.getOps()) > 1); // Append while shared (should clone) const val3: u8 = 30; @@ -3381,8 +3634,8 @@ test "append: refcount transitions" { try std.testing.expectEqual(@as(u8, 20), result2_elements[1]); try std.testing.expectEqual(@as(u8, 30), result2_elements[2]); - defer result1.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); - defer result2.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer result1.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); + defer result2.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); } test "append: capacity growth strategy" { @@ -3397,7 +3650,7 @@ test "append: capacity growth strategy" { } // Make it shared - list.incref(1, false); + list.incref(1, false, test_env.getOps()); // Append should create new list with growth-strategy determined capacity const new_val: u32 = 400; @@ -3413,8 +3666,8 @@ test "append: capacity growth strategy" { try std.testing.expectEqual(@as(u32, 300), result_elements[2]); try std.testing.expectEqual(@as(u32, 400), result_elements[3]); - defer list.decref(@alignOf(u32), @sizeOf(u32), false, rcNone, test_env.getOps()); - defer result.decref(@alignOf(u32), @sizeOf(u32), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u32), @sizeOf(u32), false, null, rcNone, test_env.getOps()); + defer result.decref(@alignOf(u32), @sizeOf(u32), false, null, rcNone, test_env.getOps()); } test "append: mixed with push operations" { @@ -3436,7 +3689,7 @@ test "append: mixed with push operations" { list = pushInPlace(list, @alignOf(u8), @sizeOf(u8), @ptrCast(@constCast(&val3)), test_env.getOps()); // Make shared - list.incref(1, false); + list.incref(1, false, test_env.getOps()); // Append while shared (should clone) const val4: u8 = 4; @@ -3451,8 +3704,8 @@ test "append: mixed with push operations" { try std.testing.expectEqual(@as(u8, 3), result_elements[2]); try std.testing.expectEqual(@as(u8, 4), result_elements[3]); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); - defer result.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); + defer result.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); } test "push and append: large scale alternating operations" { @@ -3480,7 +3733,7 @@ test "push and append: large scale alternating operations" { try std.testing.expectEqual(i, elements[i]); } - defer list.decref(@alignOf(u16), @sizeOf(u16), false, rcNone, test_env.getOps()); + defer list.decref(@alignOf(u16), @sizeOf(u16), false, null, rcNone, test_env.getOps()); } // Helper function for tests that does proper append with cloning for shared lists @@ -3492,7 +3745,7 @@ fn testAppend( test_env: *TestEnv, ) RocList { // Check if list is unique (refcount == 1) - if (list.isUnique()) { + if (list.isUnique(test_env.getOps())) { // List is unique, can mutate in place return pushInPlace(list, alignment, element_size, element, test_env.getOps()); } else { @@ -3501,7 +3754,7 @@ fn testAppend( const new_capacity = old_len + 1; // Create new list with capacity for the new element - var new_list = listWithCapacity(new_capacity, alignment, element_size, false, rcNone, test_env.getOps()); + var new_list = listWithCapacity(new_capacity, alignment, element_size, false, null, rcNone, test_env.getOps()); new_list.length = old_len + 1; // Copy existing elements @@ -3530,7 +3783,7 @@ test "append: stress test with cloning" { } // Make it shared - original.incref(1, false); + original.incref(1, false, test_env.getOps()); // Create multiple clones via append var clones: [10]RocList = undefined; @@ -3561,9 +3814,9 @@ test "append: stress test with cloning" { } // Cleanup - defer original.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer original.decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); i = 0; while (i < 10) : (i += 1) { - defer clones[i].decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + defer clones[i].decref(@alignOf(u8), @sizeOf(u8), false, null, rcNone, test_env.getOps()); } } diff --git a/src/builtins/main.zig b/src/builtins/main.zig index f8a8f689a4..0e2bb419d5 100644 --- a/src/builtins/main.zig +++ b/src/builtins/main.zig @@ -286,17 +286,3 @@ fn exportDecFn(comptime func: anytype, comptime func_name: []const u8) void { fn exportUtilsFn(comptime func: anytype, comptime func_name: []const u8) void { exportBuiltinFn(func, "utils." ++ func_name); } - -// Custom panic function, as builtin Zig version errors during LLVM verification -/// Panic function for the Roc builtins C interface. -/// This function handles runtime errors and panics in a way that's compatible -/// with the C ABI and doesn't interfere with LLVM verification. -pub fn panic(message: []const u8, stacktrace: ?*std.builtin.StackTrace, _: ?usize) noreturn { - if (comptime builtin.target.cpu.arch != .wasm32) { - std.debug.print("\nSomehow in unreachable zig panic!\nThis is a roc standard library bug\n{s}: {?}", .{ message, stacktrace }); - std.process.abort(); - } else { - // Can't call abort or print from wasm. Just leave it as unreachable. - unreachable; - } -} diff --git a/src/builtins/mod.zig b/src/builtins/mod.zig index a670440110..e2c08edaa7 100644 --- a/src/builtins/mod.zig +++ b/src/builtins/mod.zig @@ -3,6 +3,7 @@ const std = @import("std"); pub const host_abi = @import("host_abi.zig"); pub const dec = @import("dec.zig"); +pub const handlers = @import("handlers.zig"); pub const hash = @import("hash.zig"); pub const list = @import("list.zig"); pub const num = @import("num.zig"); @@ -12,6 +13,7 @@ pub const utils = @import("utils.zig"); test "builtins tests" { std.testing.refAllDecls(@import("dec.zig")); + std.testing.refAllDecls(@import("handlers.zig")); std.testing.refAllDecls(@import("hash.zig")); std.testing.refAllDecls(@import("host_abi.zig")); std.testing.refAllDecls(@import("list.zig")); diff --git a/src/builtins/num.zig b/src/builtins/num.zig index 2e59d01c69..aefd7debe4 100644 --- a/src/builtins/num.zig +++ b/src/builtins/num.zig @@ -93,7 +93,7 @@ pub fn parseIntFromStr(comptime T: type, buf: RocStr) NumParseResult(T) { /// Exports a function to parse integers from strings. pub fn exportParseInt(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(buf: RocStr) callconv(.C) NumParseResult(T) { + fn func(buf: RocStr) callconv(.c) NumParseResult(T) { return @call(.always_inline, parseIntFromStr, .{ T, buf }); } }.func; @@ -112,7 +112,7 @@ pub fn parseFloatFromStr(comptime T: type, buf: RocStr) NumParseResult(T) { /// Exports a function to parse floating-point numbers from strings. pub fn exportParseFloat(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(buf: RocStr) callconv(.C) NumParseResult(T) { + fn func(buf: RocStr) callconv(.c) NumParseResult(T) { return @call(.always_inline, parseFloatFromStr, .{ T, buf }); } }.func; @@ -122,7 +122,7 @@ pub fn exportParseFloat(comptime T: type, comptime name: []const u8) void { /// Cast an integer to a float. pub fn exportNumToFloatCast(comptime T: type, comptime F: type, comptime name: []const u8) void { const f = struct { - fn func(x: T) callconv(.C) F { + fn func(x: T) callconv(.c) F { return @floatFromInt(x); } }.func; @@ -139,7 +139,7 @@ pub fn exportPow( base: T, exp: T, roc_ops: *RocOps, - ) callconv(.C) T { + ) callconv(.c) T { switch (@typeInfo(T)) { // std.math.pow can handle ints via powi, but it turns any errors to unreachable // we want to catch overflow and report a proper error to the user @@ -165,7 +165,7 @@ pub fn exportPow( /// Check if a value is NaN. pub fn exportIsNan(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: T) callconv(.C) bool { + fn func(input: T) callconv(.c) bool { return std.math.isNan(input); } }.func; @@ -175,7 +175,7 @@ pub fn exportIsNan(comptime T: type, comptime name: []const u8) void { /// Check if a value is infinite. pub fn exportIsInfinite(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: T) callconv(.C) bool { + fn func(input: T) callconv(.c) bool { return std.math.isInf(input); } }.func; @@ -185,7 +185,7 @@ pub fn exportIsInfinite(comptime T: type, comptime name: []const u8) void { /// Check if a value is finite. pub fn exportIsFinite(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: T) callconv(.C) bool { + fn func(input: T) callconv(.c) bool { return std.math.isFinite(input); } }.func; @@ -195,7 +195,7 @@ pub fn exportIsFinite(comptime T: type, comptime name: []const u8) void { /// Compute arcsine using zig std.math. pub fn exportAsin(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: T) callconv(.C) T { + fn func(input: T) callconv(.c) T { return std.math.asin(input); } }.func; @@ -205,7 +205,7 @@ pub fn exportAsin(comptime T: type, comptime name: []const u8) void { /// Compute arccosine using zig std.math. pub fn exportAcos(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: T) callconv(.C) T { + fn func(input: T) callconv(.c) T { return std.math.acos(input); } }.func; @@ -215,7 +215,7 @@ pub fn exportAcos(comptime T: type, comptime name: []const u8) void { /// Compute arctangent using zig std.math. pub fn exportAtan(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: T) callconv(.C) T { + fn func(input: T) callconv(.c) T { return std.math.atan(input); } }.func; @@ -225,7 +225,7 @@ pub fn exportAtan(comptime T: type, comptime name: []const u8) void { /// Compute sine using zig std.math. pub fn exportSin(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: T) callconv(.C) T { + fn func(input: T) callconv(.c) T { return math.sin(input); } }.func; @@ -235,7 +235,7 @@ pub fn exportSin(comptime T: type, comptime name: []const u8) void { /// Compute cosine using zig std.math. pub fn exportCos(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: T) callconv(.C) T { + fn func(input: T) callconv(.c) T { return math.cos(input); } }.func; @@ -245,7 +245,7 @@ pub fn exportCos(comptime T: type, comptime name: []const u8) void { /// Compute tangent using zig std.math. pub fn exportTan(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: T) callconv(.C) T { + fn func(input: T) callconv(.c) T { return math.tan(input); } }.func; @@ -255,7 +255,7 @@ pub fn exportTan(comptime T: type, comptime name: []const u8) void { /// Compute natural logarithm using zig @log builtin. pub fn exportLog(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: T) callconv(.C) T { + fn func(input: T) callconv(.c) T { return @log(input); } }.func; @@ -265,7 +265,7 @@ pub fn exportLog(comptime T: type, comptime name: []const u8) void { /// Compute absolute value using zig @abs builtin. pub fn exportFAbs(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: T) callconv(.C) T { + fn func(input: T) callconv(.c) T { return @abs(input); } }.func; @@ -275,7 +275,7 @@ pub fn exportFAbs(comptime T: type, comptime name: []const u8) void { /// Compute square root using zig std.math. pub fn exportSqrt(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: T) callconv(.C) T { + fn func(input: T) callconv(.c) T { return math.sqrt(input); } }.func; @@ -285,7 +285,7 @@ pub fn exportSqrt(comptime T: type, comptime name: []const u8) void { /// Round a float to the nearest integer using zig std.math. pub fn exportRound(comptime F: type, comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: F) callconv(.C) T { + fn func(input: F) callconv(.c) T { return @as(T, @intFromFloat((math.round(input)))); } }.func; @@ -295,7 +295,7 @@ pub fn exportRound(comptime F: type, comptime T: type, comptime name: []const u8 /// Round a float down to the nearest integer using zig std.math. pub fn exportFloor(comptime F: type, comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: F) callconv(.C) T { + fn func(input: F) callconv(.c) T { return @as(T, @intFromFloat((math.floor(input)))); } }.func; @@ -305,7 +305,7 @@ pub fn exportFloor(comptime F: type, comptime T: type, comptime name: []const u8 /// Round a float up to the nearest integer using zig std.math. pub fn exportCeiling(comptime F: type, comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(input: F) callconv(.C) T { + fn func(input: F) callconv(.c) T { return @as(T, @intFromFloat((math.ceil(input)))); } }.func; @@ -322,7 +322,7 @@ pub fn exportDivCeil( a: T, b: T, roc_ops: *RocOps, - ) callconv(.C) T { + ) callconv(.c) T { return math.divCeil(T, a, b) catch { roc_ops.crash("Integer division by 0!"); }; @@ -344,7 +344,7 @@ pub fn ToIntCheckedResult(comptime T: type) type { /// Exports a function to convert to integer, checking only max bound. pub fn exportToIntCheckingMax(comptime From: type, comptime To: type, comptime name: []const u8) void { const f = struct { - fn func(input: From) callconv(.C) ToIntCheckedResult(To) { + fn func(input: From) callconv(.c) ToIntCheckedResult(To) { if (input > std.math.maxInt(To)) { return .{ .out_of_bounds = true, .value = 0 }; } @@ -357,7 +357,7 @@ pub fn exportToIntCheckingMax(comptime From: type, comptime To: type, comptime n /// Exports a function to convert to integer, checking both bounds. pub fn exportToIntCheckingMaxAndMin(comptime From: type, comptime To: type, comptime name: []const u8) void { const f = struct { - fn func(input: From) callconv(.C) ToIntCheckedResult(To) { + fn func(input: From) callconv(.c) ToIntCheckedResult(To) { if (input > std.math.maxInt(To) or input < std.math.minInt(To)) { return .{ .out_of_bounds = true, .value = 0 }; } @@ -386,7 +386,7 @@ pub fn isMultipleOf(comptime T: type, lhs: T, rhs: T) bool { /// Exports a function to check if a value is a multiple of another. pub fn exportIsMultipleOf(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(lhs: T, rhs: T) callconv(.C) bool { + fn func(lhs: T, rhs: T) callconv(.c) bool { return @call(.always_inline, isMultipleOf, .{ T, lhs, rhs }); } }.func; @@ -411,7 +411,7 @@ pub fn addWithOverflow(comptime T: type, self: T, other: T) WithOverflow(T) { /// Exports a function to add two numbers, returning overflow info. pub fn exportAddWithOverflow(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(self: T, other: T) callconv(.C) WithOverflow(T) { + fn func(self: T, other: T) callconv(.c) WithOverflow(T) { return @call(.always_inline, addWithOverflow, .{ T, self, other }); } }.func; @@ -421,7 +421,7 @@ pub fn exportAddWithOverflow(comptime T: type, comptime name: []const u8) void { /// Exports a function to add two integers, saturating on overflow. pub fn exportAddSaturatedInt(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(self: T, other: T) callconv(.C) T { + fn func(self: T, other: T) callconv(.c) T { const result = addWithOverflow(T, self, other); if (result.has_overflowed) { // We can unambiguously tell which way it wrapped, because we have N+1 bits including the overflow bit @@ -441,7 +441,7 @@ pub fn exportAddSaturatedInt(comptime T: type, comptime name: []const u8) void { /// Exports a function to add two integers, wrapping on overflow. pub fn exportAddWrappedInt(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(self: T, other: T) callconv(.C) T { + fn func(self: T, other: T) callconv(.c) T { return self +% other; } }.func; @@ -458,7 +458,7 @@ pub fn exportAddOrPanic( self: T, other: T, roc_ops: *RocOps, - ) callconv(.C) T { + ) callconv(.c) T { const result = addWithOverflow(T, self, other); if (result.has_overflowed) { roc_ops.crash("Integer addition overflowed!"); @@ -488,7 +488,7 @@ pub fn subWithOverflow(comptime T: type, self: T, other: T) WithOverflow(T) { /// Exports a function to subtract two numbers, returning overflow info. pub fn exportSubWithOverflow(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(self: T, other: T) callconv(.C) WithOverflow(T) { + fn func(self: T, other: T) callconv(.c) WithOverflow(T) { return @call(.always_inline, subWithOverflow, .{ T, self, other }); } }.func; @@ -498,7 +498,7 @@ pub fn exportSubWithOverflow(comptime T: type, comptime name: []const u8) void { /// Exports a function to subtract two integers, saturating on overflow. pub fn exportSubSaturatedInt(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(self: T, other: T) callconv(.C) T { + fn func(self: T, other: T) callconv(.c) T { const result = subWithOverflow(T, self, other); if (result.has_overflowed) { if (@typeInfo(T).int.signedness == .unsigned) { @@ -519,7 +519,7 @@ pub fn exportSubSaturatedInt(comptime T: type, comptime name: []const u8) void { /// Exports a function to subtract two integers, wrapping on overflow. pub fn exportSubWrappedInt(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(self: T, other: T) callconv(.C) T { + fn func(self: T, other: T) callconv(.c) T { return self -% other; } }.func; @@ -536,7 +536,7 @@ pub fn exportSubOrPanic( self: T, other: T, roc_ops: *RocOps, - ) callconv(.C) T { + ) callconv(.c) T { const result = subWithOverflow(T, self, other); if (result.has_overflowed) { roc_ops.crash("Integer subtraction overflowed!"); @@ -616,7 +616,7 @@ pub fn mulWithOverflow(comptime T: type, self: T, other: T) WithOverflow(T) { /// Exports a function to multiply two numbers, returning overflow info. pub fn exportMulWithOverflow(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(self: T, other: T) callconv(.C) WithOverflow(T) { + fn func(self: T, other: T) callconv(.c) WithOverflow(T) { return @call(.always_inline, mulWithOverflow, .{ T, self, other }); } }.func; @@ -626,7 +626,7 @@ pub fn exportMulWithOverflow(comptime T: type, comptime name: []const u8) void { /// Exports a function to multiply two integers, saturating on overflow. pub fn exportMulSaturatedInt(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(self: T, other: T) callconv(.C) T { + fn func(self: T, other: T) callconv(.c) T { const result = @call(.always_inline, mulWithOverflow, .{ T, self, other }); return result.value; } @@ -637,7 +637,7 @@ pub fn exportMulSaturatedInt(comptime T: type, comptime name: []const u8) void { /// Exports a function to multiply two integers, wrapping on overflow. pub fn exportMulWrappedInt(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(self: T, other: T) callconv(.C) T { + fn func(self: T, other: T) callconv(.c) T { return self *% other; } }.func; @@ -645,7 +645,7 @@ pub fn exportMulWrappedInt(comptime T: type, comptime name: []const u8) void { } /// Shifts an i128 right with zero fill. -pub fn shiftRightZeroFillI128(self: i128, other: u8) callconv(.C) i128 { +pub fn shiftRightZeroFillI128(self: i128, other: u8) callconv(.c) i128 { if (other & 0b1000_0000 > 0) { return 0; } else { @@ -654,7 +654,7 @@ pub fn shiftRightZeroFillI128(self: i128, other: u8) callconv(.C) i128 { } /// Shifts a u128 right with zero fill. -pub fn shiftRightZeroFillU128(self: u128, other: u8) callconv(.C) u128 { +pub fn shiftRightZeroFillU128(self: u128, other: u8) callconv(.c) u128 { if (other & 0b1000_0000 > 0) { return 0; } else { @@ -663,7 +663,7 @@ pub fn shiftRightZeroFillU128(self: u128, other: u8) callconv(.C) u128 { } /// Compares two i128 values, returning ordering. -pub fn compareI128(self: i128, other: i128) callconv(.C) Ordering { +pub fn compareI128(self: i128, other: i128) callconv(.c) Ordering { if (self == other) { return Ordering.EQ; } else if (self < other) { @@ -674,7 +674,7 @@ pub fn compareI128(self: i128, other: i128) callconv(.C) Ordering { } /// Compares two u128 values, returning ordering. -pub fn compareU128(self: u128, other: u128) callconv(.C) Ordering { +pub fn compareU128(self: u128, other: u128) callconv(.c) Ordering { if (self == other) { return Ordering.EQ; } else if (self < other) { @@ -685,42 +685,42 @@ pub fn compareU128(self: u128, other: u128) callconv(.C) Ordering { } /// Returns true if self < other for i128. -pub fn lessThanI128(self: i128, other: i128) callconv(.C) bool { +pub fn lessThanI128(self: i128, other: i128) callconv(.c) bool { return self < other; } /// Returns true if self <= other for i128. -pub fn lessThanOrEqualI128(self: i128, other: i128) callconv(.C) bool { +pub fn lessThanOrEqualI128(self: i128, other: i128) callconv(.c) bool { return self <= other; } /// Returns true if self > other for i128. -pub fn greaterThanI128(self: i128, other: i128) callconv(.C) bool { +pub fn greaterThanI128(self: i128, other: i128) callconv(.c) bool { return self > other; } /// Returns true if self >= other for i128. -pub fn greaterThanOrEqualI128(self: i128, other: i128) callconv(.C) bool { +pub fn greaterThanOrEqualI128(self: i128, other: i128) callconv(.c) bool { return self >= other; } /// Returns true if self < other for u128. -pub fn lessThanU128(self: u128, other: u128) callconv(.C) bool { +pub fn lessThanU128(self: u128, other: u128) callconv(.c) bool { return self < other; } /// Returns true if self <= other for u128. -pub fn lessThanOrEqualU128(self: u128, other: u128) callconv(.C) bool { +pub fn lessThanOrEqualU128(self: u128, other: u128) callconv(.c) bool { return self <= other; } /// Returns true if self > other for u128. -pub fn greaterThanU128(self: u128, other: u128) callconv(.C) bool { +pub fn greaterThanU128(self: u128, other: u128) callconv(.c) bool { return self > other; } /// Returns true if self >= other for u128. -pub fn greaterThanOrEqualU128(self: u128, other: u128) callconv(.C) bool { +pub fn greaterThanOrEqualU128(self: u128, other: u128) callconv(.c) bool { return self >= other; } @@ -734,7 +734,7 @@ pub fn exportMulOrPanic( self: T, other: T, roc_ops: *RocOps, - ) callconv(.C) T { + ) callconv(.c) T { const result = @call(.always_inline, mulWithOverflow, .{ T, self, other }); if (result.has_overflowed) { roc_ops.crash("Integer multiplication overflowed!"); @@ -749,7 +749,7 @@ pub fn exportMulOrPanic( /// Exports a function to count leading zero bits. pub fn exportCountLeadingZeroBits(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(self: T) callconv(.C) u8 { + fn func(self: T) callconv(.c) u8 { return @as(u8, @clz(self)); } }.func; @@ -759,7 +759,7 @@ pub fn exportCountLeadingZeroBits(comptime T: type, comptime name: []const u8) v /// Exports a function to count trailing zero bits. pub fn exportCountTrailingZeroBits(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(self: T) callconv(.C) u8 { + fn func(self: T) callconv(.c) u8 { return @as(u8, @ctz(self)); } }.func; @@ -769,7 +769,7 @@ pub fn exportCountTrailingZeroBits(comptime T: type, comptime name: []const u8) /// Exports a function to count one bits (population count). pub fn exportCountOneBits(comptime T: type, comptime name: []const u8) void { const f = struct { - fn func(self: T) callconv(.C) u8 { + fn func(self: T) callconv(.c) u8 { return @as(u8, @popCount(self)); } }.func; @@ -777,7 +777,7 @@ pub fn exportCountOneBits(comptime T: type, comptime name: []const u8) void { } /// Returns the bitwise parts of an f32. -pub fn f32ToParts(self: f32) callconv(.C) F32Parts { +pub fn f32ToParts(self: f32) callconv(.c) F32Parts { const u32Value = @as(u32, @bitCast(self)); return F32Parts{ .fraction = u32Value & 0x7fffff, @@ -787,7 +787,7 @@ pub fn f32ToParts(self: f32) callconv(.C) F32Parts { } /// Returns the bitwise parts of an f64. -pub fn f64ToParts(self: f64) callconv(.C) F64Parts { +pub fn f64ToParts(self: f64) callconv(.c) F64Parts { const u64Value = @as(u64, @bitCast(self)); return F64Parts{ .fraction = u64Value & 0xfffffffffffff, @@ -797,42 +797,42 @@ pub fn f64ToParts(self: f64) callconv(.C) F64Parts { } /// Constructs an f32 from its bitwise parts. -pub fn f32FromParts(parts: F32Parts) callconv(.C) f32 { +pub fn f32FromParts(parts: F32Parts) callconv(.c) f32 { return @as(f32, @bitCast(parts.fraction & 0x7fffff | (@as(u32, parts.exponent) << 23) | (@as(u32, @intFromBool(parts.sign)) << 31))); } /// Constructs an f64 from its bitwise parts. -pub fn f64FromParts(parts: F64Parts) callconv(.C) f64 { +pub fn f64FromParts(parts: F64Parts) callconv(.c) f64 { return @as(f64, @bitCast(parts.fraction & 0xfffffffffffff | (@as(u64, parts.exponent & 0x7ff) << 52) | (@as(u64, @intFromBool(parts.sign)) << 63))); } /// Returns the bit pattern of an f32 as u32. -pub fn f32ToBits(self: f32) callconv(.C) u32 { +pub fn f32ToBits(self: f32) callconv(.c) u32 { return @as(u32, @bitCast(self)); } /// Returns the bit pattern of an f64 as u64. -pub fn f64ToBits(self: f64) callconv(.C) u64 { +pub fn f64ToBits(self: f64) callconv(.c) u64 { return @as(u64, @bitCast(self)); } /// Returns the bit pattern of an i128 as u128. -pub fn i128ToBits(self: i128) callconv(.C) u128 { +pub fn i128ToBits(self: i128) callconv(.c) u128 { return @as(u128, @bitCast(self)); } /// Constructs an f32 from its bit pattern. -pub fn f32FromBits(bits: u32) callconv(.C) f32 { +pub fn f32FromBits(bits: u32) callconv(.c) f32 { return @as(f32, @bitCast(bits)); } /// Constructs an f64 from its bit pattern. -pub fn f64FromBits(bits: u64) callconv(.C) f64 { +pub fn f64FromBits(bits: u64) callconv(.c) f64 { return @as(f64, @bitCast(bits)); } /// Constructs an i128 from its bit pattern. -pub fn i128FromBits(bits: u128) callconv(.C) i128 { +pub fn i128FromBits(bits: u128) callconv(.c) i128 { return @as(i128, @bitCast(bits)); } diff --git a/src/builtins/roc/Bool.roc b/src/builtins/roc/Bool.roc deleted file mode 100644 index 43c45295cb..0000000000 --- a/src/builtins/roc/Bool.roc +++ /dev/null @@ -1,80 +0,0 @@ -module [Bool, Eq, true, false, not, is_eq, is_not_eq] - -## Defines a type that can be compared for total equality. -## -## Total equality means that all values of the type can be compared to each -## other, and two values `a`, `b` are identical if and only if `is_eq(a, b)` is -## `Bool.true`. -## -## Not all types support total equality. For example, [`F32`](Num#F32) and [`F64`](Num#F64) can -## be a `NaN` ([Not a Number](https://en.wikipedia.org/wiki/NaN)), and the -## [IEEE-754](https://en.wikipedia.org/wiki/IEEE_754) floating point standard -## specifies that two `NaN`s are not equal. -Eq(a) : a - where - ## Returns `Bool.true` if the input values are equal. This is - ## equivalent to the logic - ## [XNOR](https://en.wikipedia.org/wiki/Logical_equality) gate. The infix - ## operator `==` can be used as shorthand for `Bool.is_eq`. - ## - ## **Note** that when `is_eq` is determined by the Roc compiler, values are - ## compared using structural equality. The rules for this are as follows: - ## - ## 1. Tags are equal if their name and also contents are equal. - ## 2. Records are equal if their fields are equal. - ## 3. The collections [Str], [List], [Dict], and [Set] are equal iff they - ## are the same length and their elements are equal. - ## 4. [Num] values are equal if their numbers are equal. However, if both - ## inputs are *NaN* then `is_eq` returns `Bool.false`. Refer to `Num.is_nan` - ## for more detail. - ## 5. Functions cannot be compared for structural equality, therefore Roc - ## cannot derive `is_eq` for types that contain functions. - a.is_eq(a) -> Bool, - -## Represents the boolean true and false using an nominal type. -## `Bool` implements the `Eq` ability. -Bool := [True, False] - -## The boolean true value. -true : Bool -true = Bool.True - -## The boolean false value. -false : Bool -false = Bool.False - -## Satisfies the interface of `Eq` -is_eq : Bool, Bool -> Bool -is_eq = |b1, b2| match (b1, b2) { - (Bool.True, Bool.True) => true - (Bool.False, Bool.False) => true - _ => false -} - -## 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`. -## ```roc -## expect Bool.not(Bool.false) == Bool.true -## expect Bool.false != Bool.true -## ``` -not : Bool -> Bool -not = |b| match b { - Bool.True => false - Bool.False => true -} - -## This will call the function `Bool.is_eq` on the inputs, and then `Bool.not` -## on the result. The is equivalent to the logic -## [XOR](https://en.wikipedia.org/wiki/Exclusive_or) gate. The infix operator -## `!=` can also be used as shorthand for `Bool.is_not_eq`. -## -## **Note** that `is_not_eq` does not accept arguments whose types contain -## functions. -## ```roc -## expect Bool.is_not_eq(Bool.false, Bool.true) == Bool.true -## expect (Bool.false != Bool.false) == Bool.false -## expect "Apples" != "Oranges" -## ``` -is_not_eq : a, a -> Bool where a.Eq -is_not_eq = |a, b| not(a.is_eq(b)) diff --git a/src/builtins/roc/Result.roc b/src/builtins/roc/Result.roc deleted file mode 100644 index bf0e61fc99..0000000000 --- a/src/builtins/roc/Result.roc +++ /dev/null @@ -1,177 +0,0 @@ -module [ - Result, - is_ok, - is_err, - is_eq, - map_ok, - map_err, - on_err, - on_err!, - map_both, - map2, - try, - with_default, -] - -import Bool exposing [Bool.*] - -## The result of an operation that could fail: either the operation went -## okay, or else there was an error of some sort. -Result(ok, err) := [Ok(ok), Err(err)] - -## Returns `Bool.true` if the result indicates a success, else returns `Bool.false`. -## ```roc -## Ok(5).is_ok() -## ``` -is_ok : Result(ok, err) -> Bool -is_ok = |result| match result { - Result.Ok(_) => Bool.true - Result.Err(_) => Bool.false -} - -## Returns `Bool.true` if the result indicates a failure, else returns `Bool.false`. -## ```roc -## Err("uh oh").is_err() -## ``` -is_err : Result(ok, err) -> Bool -is_err = |result| match result { - Result.Ok(_) => Bool.false - Result.Err(_) => Bool.true -} - -## If the result is `Ok`, returns the value it holds. Otherwise, returns -## the given default value. -## -## Note: This function should be used sparingly, because it hides that an error -## happened, which will make debugging harder. Prefer using `?` to forward errors or -## handle them explicitly with `when`. -## ```roc -## Err("uh oh").with_default(42) # = 42 -## -## Ok(7).with_default(42) # = 7 -## ``` -with_default : Result(ok, err), ok -> ok -with_default = |result, default| match result { - Result.Ok(value) => value - Result.Err(_) => default -} - -## If the result is `Ok`, transforms the value it holds by running a conversion -## function on it. Then returns a new `Ok` holding the transformed value. If the -## result is `Err`, this has no effect. Use [map_err] to transform an `Err`. -## ```roc -## Ok(12).map_ok(Num.neg) # = Ok(-12) -## -## Err("yipes!").map_ok(Num.neg) # = Err("yipes!") -## ``` -## -## Functions like `map` are common in Roc; see for example [List.map], -## `Set.map`, and `Dict.map`. -map_ok : Result(a, err), (a -> b) -> Result(b, err) -map_ok = |result, transform| match result { - Result.Ok(v) => Result.Ok(transform(v)) - Result.Err(e) => Result.Err(e) -} - -## If the result is `Err`, transforms the value it holds by running a conversion -## function on it. Then returns a new `Err` holding the transformed value. If -## the result is `Ok`, this has no effect. Use [map] to transform an `Ok`. -## ```roc -## [].last().map_err(|_| ProvidedListIsEmpty) # = Err(ProvidedListIsEmpty) -## -## [4].last().map_err(|_| ProvidedListIsEmpty) # = Ok(4) -## ``` -map_err : Result(ok, a), (a -> b) -> Result(ok, b) -map_err = |result, transform| match result { - Result.Ok(v) => Result.Ok(v) - Result.Err(e) => Result.Err(transform(e)) -} - -## If the result is `Err`, transforms the entire result by running a conversion -## function on the value the `Err` holds. Then returns that new result. If the -## result is `Ok`, this has no effect. Use `?` or [try] to transform an `Ok`. -## ```roc -## Result.on_err(Ok(10), Str.to_u64) # = Ok(10) -## -## Result.on_err(Err("42"), Str.to_u64) # = Ok(42) -## -## Result.on_err(Err("string"), Str.to_u64) # = Err(InvalidNumStr) -## ``` -on_err : Result(a, err), (err -> Result(a, other_err)) -> Result(a, other_err) -on_err = |result, transform| match result { - Result.Ok(v) => Result.Ok(v) - Result.Err(e) => transform(e) -} - -expect Result.on_err(Ok(10), Str.to_u64) == Result.Ok(10) -expect Result.on_err(Err("42"), Str.to_u64) == Result.Ok(42) -expect Result.on_err(Err("string"), Str.to_u64) == Result.Err(InvalidNumStr) - -## Like [on_err], but it allows the transformation function to produce effects. -## -## ```roc -## Err("missing user").on_err(|msg| { -## Stdout.line!("ERROR: ${msg}")? -## Err(msg) -## }) -## ``` -on_err! : Result(a, err), (err => Result(a, other_err)) => Result(a, other_err) -on_err! = |result, transform!| match result { - Result.Ok(v) => Result.Ok(v) - Result.Err(e) => transform!(e) -} - -## Maps both the `Ok` and `Err` values of a `Result` to new values. -map_both : Result(ok1, err1), (ok1 -> ok2), (err1 -> err2) -> Result(ok2, err2) -map_both = |result, ok_transform, err_transform| match result { - Result. Ok(val) => Result.Ok(ok_transform(val)) - Result. Err(err) => Result.Err(err_transform(err)) -} - -## Maps the `Ok` values of two `Result`s to a new value using a given transformation, -## or returns the first `Err` value encountered. -map2 : Result(a, err), Result(b, err), (a, b -> c) -> Result(c, err) -map2 = |first_result, second_result, transform| match (first_result, second_result) { - (Result.Ok(first), Result.Ok(second)) => Ok(transform(first, second)) - (Result.Err(err), _) => Result.Err(err) - (_, Result.Err(err)) => Result.Err(err) -} - -## If the result is `Ok`, transforms the entire result by running a conversion -## function on the value the `Ok` holds. Then returns that new result. If the -## result is `Err`, this has no effect. Use `on_err` to transform an `Err`. -## -## We recommend using `?` instead of `try`, it makes the code easier to read. -## ```roc -## Ok(-1).try(|num| if num < 0 then Err("negative!") else Ok(-num)) # = Err("negative!") -## -## Ok(1).try(|num| if num < 0 then Err("negative!") else Ok(-num)) # = Ok(-1) -## -## Err("yipes!").try(|num| if num < 0 then Err("negative!") else Ok(-num)) # = Err("yipes!") -## ``` -try : Result(a, err), (a -> Result(b, err)) -> Result(b, err) -try = |result, transform| match result { - Result.Ok(v) => transform(v) - Result.Err(e) => Result.Err(e) -} - -expect Ok(-1).try(|num| if num < 0 then Err("negative!") else Ok(-num)) == Result.Err("negative!") -expect Ok(1).try(|num| if num < 0 then Err("negative!") else Ok(-num)) == Result.Ok(-1) -expect Err("yipes!").try(|num| if num < 0 then Err("negative!") else Ok(-num)) == Result.Err("yipes!") - -## Implementation of [Bool.Eq]. Checks if two results that have both `ok` and `err` types that are `Eq` are themselves equal. -## -## ```roc -## Ok("Hello").is_eq(Ok("Hello")) -## ``` -is_eq : Result(ok, err), Result(ok, err) -> Bool where ok.Eq, err.Eq -is_eq = |r1, r2| match (r1, r2) { - (Result.Ok(ok1), Result.Ok(ok2)) => ok1 == ok2 - (Result.Err(err1), Result.Err(err2)) => err1 == err2 -} - -expect Result.Ok(1) == Result.Ok(1) -expect Result.Ok(2) != Result.Ok(1) -expect Result.Err("Foo") == Result.Err("Foo") -expect Result.Err("Bar") != Result.Err("Foo") -expect Result.Ok("Foo") != Result.Err("Foo") diff --git a/src/builtins/roc/main.roc b/src/builtins/roc/main.roc deleted file mode 100644 index 7b4c819ae5..0000000000 --- a/src/builtins/roc/main.roc +++ /dev/null @@ -1,6 +0,0 @@ -package - [ - Bool, - Result, - ] - {} \ No newline at end of file diff --git a/src/builtins/sort.zig b/src/builtins/sort.zig index a5e012447e..b27093185b 100644 --- a/src/builtins/sort.zig +++ b/src/builtins/sort.zig @@ -9,15 +9,16 @@ const std = @import("std"); const GT = Ordering.GT; const LT = Ordering.LT; const EQ = Ordering.EQ; -const Ordering = @import("utils.zig").Ordering; +const utils = @import("utils.zig"); +const Ordering = utils.Ordering; const RocOps = @import("host_abi.zig").RocOps; const testing = std.testing; /// TODO pub const Opaque = ?[*]u8; -const CompareFn = *const fn (Opaque, Opaque, Opaque) callconv(.C) u8; -const CopyFn = *const fn (Opaque, Opaque) callconv(.C) void; -const IncN = *const fn (?[*]u8, usize) callconv(.C) void; +const CompareFn = *const fn (Opaque, Opaque, Opaque) callconv(.c) u8; +const CopyFn = *const fn (Opaque, Opaque) callconv(.c) void; +const IncN = *const fn (?*anyopaque, ?[*]u8, usize) callconv(.c) void; /// Any size larger than the max element buffer will be sorted indirectly via pointers. /// TODO: tune this. I think due to llvm inlining the compare, the value likely should be lower. @@ -35,8 +36,9 @@ comptime { std.debug.assert(MAX_ELEMENT_BUFFER_SIZE % BufferAlign == 0); } -// ================ Fluxsort ================================================== +// Fluxsort // The high level fluxsort functions. + /// TODO: document fluxsort pub fn fluxsort( array: [*]u8, @@ -44,6 +46,7 @@ pub fn fluxsort( cmp: CompareFn, cmp_data: Opaque, data_is_owned_runtime: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, element_width: usize, alignment: u32, @@ -58,18 +61,18 @@ pub fn fluxsort( // Also, for numeric types, inlining the compare function can be a 2x perf gain. if (len < 132) { // Just quadsort it. - quadsort(array, len, cmp, cmp_data, data_is_owned_runtime, inc_n_data, element_width, alignment, copy, roc_ops); + quadsort(array, len, cmp, cmp_data, data_is_owned_runtime, inc_n_context, inc_n_data, element_width, alignment, copy, roc_ops); } else if (element_width <= MAX_ELEMENT_BUFFER_SIZE) { if (data_is_owned_runtime) { - fluxsort_direct(array, len, cmp, cmp_data, element_width, alignment, copy, true, inc_n_data, false, roc_ops); + fluxsort_direct(array, len, cmp, cmp_data, element_width, alignment, copy, true, inc_n_context, inc_n_data, false, roc_ops); } else { - fluxsort_direct(array, len, cmp, cmp_data, element_width, alignment, copy, false, inc_n_data, false, roc_ops); + fluxsort_direct(array, len, cmp, cmp_data, element_width, alignment, copy, false, inc_n_context, inc_n_data, false, roc_ops); } } else { const alloc_ptr = roc_ops.alloc(len * @sizeOf(usize), @alignOf(usize)); // Build list of pointers to sort. - const arr_ptr = @as([*]Opaque, @ptrCast(@alignCast(alloc_ptr))); + const arr_ptr: [*]Opaque = utils.alignedPtrCast([*]Opaque, @as([*]u8, @ptrCast(alloc_ptr)), @src()); defer roc_ops.dealloc(alloc_ptr, @alignOf(usize)); for (0..len) |i| { arr_ptr[i] = array + i * element_width; @@ -77,9 +80,9 @@ pub fn fluxsort( // Sort. if (data_is_owned_runtime) { - fluxsort_direct(@ptrCast(arr_ptr), len, cmp, cmp_data, @sizeOf(usize), @alignOf(usize), &pointer_copy, true, inc_n_data, true, roc_ops); + fluxsort_direct(@ptrCast(arr_ptr), len, cmp, cmp_data, @sizeOf(usize), @alignOf(usize), &pointer_copy, true, inc_n_context, inc_n_data, true, roc_ops); } else { - fluxsort_direct(@ptrCast(arr_ptr), len, cmp, cmp_data, @sizeOf(usize), @alignOf(usize), &pointer_copy, false, inc_n_data, true, roc_ops); + fluxsort_direct(@ptrCast(arr_ptr), len, cmp, cmp_data, @sizeOf(usize), @alignOf(usize), &pointer_copy, false, inc_n_context, inc_n_data, true, roc_ops); } const collect_ptr = roc_ops.alloc(len * element_width, alignment); @@ -103,13 +106,14 @@ fn fluxsort_direct( alignment: u32, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, roc_ops: *RocOps, ) void { const swap = roc_ops.alloc(len * element_width, alignment); - flux_analyze(array, len, @as([*]u8, @ptrCast(swap)), len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_analyze(array, len, @as([*]u8, @ptrCast(swap)), len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); roc_ops.dealloc(swap, alignment); @@ -152,6 +156,7 @@ fn flux_analyze( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { @@ -179,19 +184,19 @@ fn flux_analyze( if (quad1 < quad2) { // Must inc here, due to being in a branch. - const gt = compare_inc(cmp, cmp_data, ptr_b, ptr_b + element_width, data_is_owned, inc_n_data, indirect) == GT; + const gt = compare_inc(cmp, cmp_data, ptr_b, ptr_b + element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT; balance_b += @intFromBool(gt); ptr_b += element_width; } if (quad1 < quad3) { // Must inc here, due to being in a branch. - const gt = compare_inc(cmp, cmp_data, ptr_c, ptr_c + element_width, data_is_owned, inc_n_data, indirect) == GT; + const gt = compare_inc(cmp, cmp_data, ptr_c, ptr_c + element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT; balance_c += @intFromBool(gt); ptr_c += element_width; } if (quad1 < quad4) { // Must inc here, due to being in a branch. - balance_d += @intFromBool(compare_inc(cmp, cmp_data, ptr_d, ptr_d + element_width, data_is_owned, inc_n_data, indirect) == GT); + balance_d += @intFromBool(compare_inc(cmp, cmp_data, ptr_d, ptr_d + element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT); ptr_d += element_width; } @@ -199,7 +204,7 @@ fn flux_analyze( while (count > 132) : (count -= 128) { // 32*4 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 32 * 4); + inc_n_data(inc_n_context, cmp_data, 32 * 4); } var sum_a: u8 = 0; var sum_b: u8 = 0; @@ -245,7 +250,7 @@ fn flux_analyze( if (count > 7) { // 4*divCeil(count-7, 4) guaranteed compares. const n: usize = std.math.divCeil(usize, count - 7, 4) catch unreachable; - inc_n_data(cmp_data, 4 * n); + inc_n_data(inc_n_context, cmp_data, 4 * n); } } while (count > 7) : (count -= 4) { @@ -263,9 +268,9 @@ fn flux_analyze( if (count == 0) { // The whole list may be ordered. Cool! - if (compare_inc(cmp, cmp_data, ptr_a, ptr_a + element_width, data_is_owned, inc_n_data, indirect) != GT and - compare_inc(cmp, cmp_data, ptr_b, ptr_b + element_width, data_is_owned, inc_n_data, indirect) != GT and - compare_inc(cmp, cmp_data, ptr_c, ptr_c + element_width, data_is_owned, inc_n_data, indirect) != GT) + if (compare_inc(cmp, cmp_data, ptr_a, ptr_a + element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and + compare_inc(cmp, cmp_data, ptr_b, ptr_b + element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and + compare_inc(cmp, cmp_data, ptr_c, ptr_c + element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) return; } @@ -279,7 +284,7 @@ fn flux_analyze( if (reversed_any) { // 3 compares guaranteed. if (data_is_owned) { - inc_n_data(cmp_data, 3); + inc_n_data(inc_n_context, cmp_data, 3); } const span1: u3 = @intFromBool(reversed_a and reversed_b) * @intFromBool(compare(cmp, cmp_data, ptr_a, ptr_a + element_width, indirect) == GT); const span2: u3 = @intFromBool(reversed_b and reversed_c) * @intFromBool(compare(cmp, cmp_data, ptr_b, ptr_b + element_width, indirect) == GT); @@ -363,105 +368,105 @@ fn flux_analyze( } switch (ordered_a | (ordered_b << 1) | (ordered_c << 2) | (ordered_d << 3)) { 0 => { - flux_partition(array, swap, array, swap + len * element_width, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_partition(array, swap, array, swap + len * element_width, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); return; }, 1 => { if (balance_a != 0) - quadsort_swap(array, quad1, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - flux_partition(ptr_a + element_width, swap, ptr_a + element_width, swap + (quad2 + half2) * element_width, quad2 + half2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(array, quad1, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + flux_partition(ptr_a + element_width, swap, ptr_a + element_width, swap + (quad2 + half2) * element_width, quad2 + half2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, 2 => { - flux_partition(array, swap, array, swap + quad1 * element_width, quad1, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_partition(array, swap, array, swap + quad1 * element_width, quad1, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); if (balance_b != 0) - quadsort_swap(ptr_a + element_width, quad2, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - flux_partition(ptr_b + element_width, swap, ptr_b + element_width, swap + half2 * element_width, half2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(ptr_a + element_width, quad2, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + flux_partition(ptr_b + element_width, swap, ptr_b + element_width, swap + half2 * element_width, half2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, 3 => { if (balance_a != 0) - quadsort_swap(array, quad1, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(array, quad1, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); if (balance_b != 0) - quadsort_swap(ptr_a + element_width, quad2, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - flux_partition(ptr_b + element_width, swap, ptr_b + element_width, swap + half2 * element_width, half2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(ptr_a + element_width, quad2, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + flux_partition(ptr_b + element_width, swap, ptr_b + element_width, swap + half2 * element_width, half2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, 4 => { - flux_partition(array, swap, array, swap + half1 * element_width, half1, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_partition(array, swap, array, swap + half1 * element_width, half1, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); if (balance_c != 0) - quadsort_swap(ptr_b + element_width, quad3, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - flux_partition(ptr_c + element_width, swap, ptr_c + element_width, swap + quad4 * element_width, quad4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(ptr_b + element_width, quad3, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + flux_partition(ptr_c + element_width, swap, ptr_c + element_width, swap + quad4 * element_width, quad4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, 8 => { - flux_partition(array, swap, array, swap + (half1 + quad3) * element_width, (half1 + quad3), cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_partition(array, swap, array, swap + (half1 + quad3) * element_width, (half1 + quad3), cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); if (balance_d != 0) - quadsort_swap(ptr_c + element_width, quad4, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(ptr_c + element_width, quad4, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, 9 => { if (balance_a != 0) - quadsort_swap(array, quad1, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - flux_partition(ptr_a + element_width, swap, ptr_a + element_width, swap + (quad2 + quad3) * element_width, quad2 + quad3, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(array, quad1, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + flux_partition(ptr_a + element_width, swap, ptr_a + element_width, swap + (quad2 + quad3) * element_width, quad2 + quad3, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); if (balance_d != 0) - quadsort_swap(ptr_c + element_width, quad4, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(ptr_c + element_width, quad4, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, 12 => { - flux_partition(array, swap, array, swap + half1 * element_width, half1, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_partition(array, swap, array, swap + half1 * element_width, half1, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); if (balance_c != 0) - quadsort_swap(ptr_b + element_width, quad3, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(ptr_b + element_width, quad3, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); if (balance_d != 0) - quadsort_swap(ptr_c + element_width, quad4, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(ptr_c + element_width, quad4, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, 5, 6, 7, 10, 11, 13, 14, 15 => { if (ordered_a != 0) { if (balance_a != 0) - quadsort_swap(array, quad1, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(array, quad1, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } else { - flux_partition(array, swap, array, swap + quad1 * element_width, quad1, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_partition(array, swap, array, swap + quad1 * element_width, quad1, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } if (ordered_b != 0) { if (balance_b != 0) - quadsort_swap(ptr_a + element_width, quad2, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(ptr_a + element_width, quad2, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } else { - flux_partition(ptr_a + element_width, swap, ptr_a + element_width, swap + quad2 * element_width, quad2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_partition(ptr_a + element_width, swap, ptr_a + element_width, swap + quad2 * element_width, quad2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } if (ordered_c != 0) { if (balance_c != 0) - quadsort_swap(ptr_b + element_width, quad3, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(ptr_b + element_width, quad3, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } else { - flux_partition(ptr_b + element_width, swap, ptr_b + element_width, swap + quad3 * element_width, quad3, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_partition(ptr_b + element_width, swap, ptr_b + element_width, swap + quad3 * element_width, quad3, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } if (ordered_d != 0) { if (balance_d != 0) - quadsort_swap(ptr_c + element_width, quad4, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(ptr_c + element_width, quad4, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } else { - flux_partition(ptr_c + element_width, swap, ptr_c + element_width, swap + quad4 * element_width, quad4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_partition(ptr_c + element_width, swap, ptr_c + element_width, swap + quad4 * element_width, quad4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } }, } // Final Merging of sorted partitions. - if (compare_inc(cmp, cmp_data, ptr_a, ptr_a + element_width, data_is_owned, inc_n_data, indirect) != GT) { - if (compare_inc(cmp, cmp_data, ptr_c, ptr_c + element_width, data_is_owned, inc_n_data, indirect) != GT) { - if (compare_inc(cmp, cmp_data, ptr_b, ptr_b + element_width, data_is_owned, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, ptr_a, ptr_a + element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, ptr_c, ptr_c + element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, ptr_b, ptr_b + element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { // Lucky us, everything sorted. return; } @memcpy(swap[0..(len * element_width)], array[0..(len * element_width)]); } else { // First half sorted, second half needs merge. - cross_merge(swap + half1 * element_width, array + half1 * element_width, quad3, quad4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + cross_merge(swap + half1 * element_width, array + half1 * element_width, quad3, quad4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); @memcpy(swap[0..(half1 * element_width)], array[0..(half1 * element_width)]); } } else { - if (compare_inc(cmp, cmp_data, ptr_c, ptr_c + element_width, data_is_owned, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, ptr_c, ptr_c + element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { // First half needs merge, second half sorted. @memcpy((swap + half1 * element_width)[0..(half2 * element_width)], (array + half1 * element_width)[0..(half2 * element_width)]); - cross_merge(swap, array, quad1, quad2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + cross_merge(swap, array, quad1, quad2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } else { // Both halves need merge. - cross_merge(swap + half1 * element_width, ptr_b + element_width, quad3, quad4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - cross_merge(swap, array, quad1, quad2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + cross_merge(swap + half1 * element_width, ptr_b + element_width, quad3, quad4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + cross_merge(swap, array, quad1, quad2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } } // Merge bach to original list. - cross_merge(array, swap, half1, half2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + cross_merge(array, swap, half1, half2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } fn flux_partition( @@ -475,6 +480,7 @@ fn flux_partition( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { @@ -491,28 +497,28 @@ fn flux_partition( pivot_ptr -= element_width; if (len <= 2048) { - median_of_nine(x_ptr, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, pivot_ptr, indirect); + median_of_nine(x_ptr, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, pivot_ptr, indirect); } else { - median_of_cube_root(array, swap, x_ptr, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, &generic, pivot_ptr, indirect); + median_of_cube_root(array, swap, x_ptr, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, &generic, pivot_ptr, indirect); if (generic) { // Tons of identical elements, quadsort. if (x_ptr == swap) { @memcpy(array[0..(len * element_width)], swap[0..(len * element_width)]); } - quadsort_swap(array, len, swap, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(array, len, swap, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); return; } } - if (arr_len != 0 and compare_inc(cmp, cmp_data, pivot_ptr + element_width, pivot_ptr, data_is_owned, inc_n_data, indirect) != GT) { + if (arr_len != 0 and compare_inc(cmp, cmp_data, pivot_ptr + element_width, pivot_ptr, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { // pivot equals the last pivot, reverse partition and everything is done. - flux_reverse_partition(array, swap, array, pivot_ptr, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_reverse_partition(array, swap, array, pivot_ptr, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); return; } // arr_len is elements <= pivot. // swap_len is elements > pivot. - arr_len = flux_default_partition(array, swap, x_ptr, pivot_ptr, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + arr_len = flux_default_partition(array, swap, x_ptr, pivot_ptr, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); swap_len = len - arr_len; // If highly imbalanced try a different strategy. @@ -520,21 +526,21 @@ fn flux_partition( if (arr_len == 0) return; if (swap_len == 0) { - flux_reverse_partition(array, swap, array, pivot_ptr, arr_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_reverse_partition(array, swap, array, pivot_ptr, arr_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); return; } @memcpy((array + arr_len * element_width)[0..(swap_len * element_width)], swap[0..(swap_len * element_width)]); - quadsort_swap(array + arr_len * element_width, swap_len, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(array + arr_len * element_width, swap_len, swap, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } else { - flux_partition(array + arr_len * element_width, swap, swap, pivot_ptr, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_partition(array + arr_len * element_width, swap, swap, pivot_ptr, swap_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } // If highly imbalanced try a different strategy if (swap_len <= arr_len / 32 or arr_len <= FLUX_OUT) { if (arr_len <= FLUX_OUT) { - quadsort_swap(array, arr_len, swap, arr_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(array, arr_len, swap, arr_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } else { - flux_reverse_partition(array, swap, array, pivot_ptr, arr_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_reverse_partition(array, swap, array, pivot_ptr, arr_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } return; } @@ -564,6 +570,7 @@ pub fn flux_default_partition( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) usize { @@ -574,7 +581,7 @@ pub fn flux_default_partition( // len guaranteed compares if (data_is_owned) { - inc_n_data(cmp_data, len); + inc_n_data(inc_n_context, cmp_data, len); } var run: usize = 0; var a: usize = 8; @@ -612,8 +619,8 @@ pub fn flux_default_partition( a = len - m; @memcpy((array + m * element_width)[0..(a * element_width)], swap[0..(a * element_width)]); - quadsort_swap(array + m * element_width, a, swap, a, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - quadsort_swap(array, m, swap, m, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(array + m * element_width, a, swap, a, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + quadsort_swap(array, m, swap, m, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); return 0; } @@ -633,6 +640,7 @@ pub fn flux_reverse_partition( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { @@ -643,7 +651,7 @@ pub fn flux_reverse_partition( // len guaranteed compares if (data_is_owned) { - inc_n_data(cmp_data, len); + inc_n_data(inc_n_context, cmp_data, len); } for (0..(len / 8)) |_| { inline for (0..8) |_| { @@ -666,13 +674,13 @@ pub fn flux_reverse_partition( @memcpy((array + arr_len * element_width)[0..(swap_len * element_width)], swap[0..(swap_len * element_width)]); if (swap_len <= arr_len / 16 or arr_len <= FLUX_OUT) { - quadsort_swap(array, arr_len, swap, arr_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(array, arr_len, swap, arr_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); return; } - flux_partition(array, swap, array, pivot, arr_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + flux_partition(array, swap, array, pivot, arr_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } -// ================ Pivot Selection =========================================== +// Pivot Selection // Used for selecting the quicksort pivot for various sized arrays. /// Returns the median of an array taking roughly cube root samples. @@ -689,6 +697,7 @@ pub fn median_of_cube_root( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, generic: *bool, out: [*]u8, @@ -709,12 +718,12 @@ pub fn median_of_cube_root( } cbrt /= 2; - quadsort_swap(swap_ptr, cbrt, swap_ptr + cbrt * 2 * element_width, cbrt, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - quadsort_swap(swap_ptr + cbrt * element_width, cbrt, swap_ptr + cbrt * 2 * element_width, cbrt, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quadsort_swap(swap_ptr, cbrt, swap_ptr + cbrt * 2 * element_width, cbrt, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + quadsort_swap(swap_ptr + cbrt * element_width, cbrt, swap_ptr + cbrt * 2 * element_width, cbrt, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); - generic.* = compare_inc(cmp, cmp_data, swap_ptr + (cbrt * 2 - 1) * element_width, swap_ptr, data_is_owned, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, swap_ptr + (cbrt - 1) * element_width, swap_ptr, data_is_owned, inc_n_data, indirect) != GT; + generic.* = compare_inc(cmp, cmp_data, swap_ptr + (cbrt * 2 - 1) * element_width, swap_ptr, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, swap_ptr + (cbrt - 1) * element_width, swap_ptr, data_is_owned, inc_n_context, inc_n_data, indirect) != GT; - binary_median(swap_ptr, swap_ptr + cbrt * element_width, cbrt, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, out, indirect); + binary_median(swap_ptr, swap_ptr + cbrt * element_width, cbrt, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, out, indirect); } /// Returns the median of 9 evenly distributed elements from a list. @@ -726,6 +735,7 @@ pub fn median_of_nine( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, out: [*]u8, comptime indirect: bool, @@ -741,19 +751,19 @@ pub fn median_of_nine( arr_ptr += offset; } - trim_four(swap_ptr, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - trim_four(swap_ptr + 4 * element_width, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + trim_four(swap_ptr, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + trim_four(swap_ptr + 4 * element_width, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); copy(swap_ptr, swap_ptr + 5 * element_width); copy(swap_ptr + 3 * element_width, swap_ptr + 8 * element_width); - trim_four(swap_ptr, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + trim_four(swap_ptr, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); copy(swap_ptr, swap_ptr + 6 * element_width); // 3 guaranteed compares if (data_is_owned) { - inc_n_data(cmp_data, 3); + inc_n_data(inc_n_context, cmp_data, 3); } const x: usize = @intFromBool(compare(cmp, cmp_data, swap_ptr + 0 * element_width, swap_ptr + 1 * element_width, indirect) == GT); const y: usize = @intFromBool(compare(cmp, cmp_data, swap_ptr + 0 * element_width, swap_ptr + 2 * element_width, indirect) == GT); @@ -772,6 +782,7 @@ pub fn trim_four( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { @@ -780,7 +791,7 @@ pub fn trim_four( // 4 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 4); + inc_n_data(inc_n_context, cmp_data, 4); } var ptr_a = initial_ptr_a; { @@ -825,6 +836,7 @@ pub fn binary_median( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, out: [*]u8, comptime indirect: bool, @@ -833,7 +845,7 @@ pub fn binary_median( if (data_is_owned) { // We need to increment log2 of len times. const log2 = @bitSizeOf(usize) - @clz(len); - inc_n_data(cmp_data, log2); + inc_n_data(inc_n_context, cmp_data, log2); } var ptr_a = initial_ptr_a; var ptr_b = initial_ptr_b; @@ -849,7 +861,7 @@ pub fn binary_median( copy(out, from); } -// ================ Quadsort ================================================== +// Quadsort // The high level quadsort functions. /// A version of quadsort given pre-allocated swap memory. @@ -865,15 +877,16 @@ pub fn quadsort_swap( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { if (len < 96) { - tail_swap(array, len, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - } else if (quad_swap(array, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect) != .sorted) { - const block_len = quad_merge(array, len, swap, swap_len, 32, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + tail_swap(array, len, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + } else if (quad_swap(array, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect) != .sorted) { + const block_len = quad_merge(array, len, swap, swap_len, 32, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); - rotate_merge(array, len, swap, swap_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + rotate_merge(array, len, swap, swap_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } } @@ -884,6 +897,7 @@ pub fn quadsort( cmp: CompareFn, cmp_data: Opaque, data_is_owned_runtime: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, element_width: usize, alignment: u32, @@ -898,15 +912,15 @@ pub fn quadsort( // Also, for numeric types, inlining the compare function can be a 2x perf gain. if (element_width <= MAX_ELEMENT_BUFFER_SIZE) { if (data_is_owned_runtime) { - quadsort_direct(array, len, cmp, cmp_data, element_width, alignment, copy, true, inc_n_data, false, roc_ops); + quadsort_direct(array, len, cmp, cmp_data, element_width, alignment, copy, true, inc_n_context, inc_n_data, false, roc_ops); } else { - quadsort_direct(array, len, cmp, cmp_data, element_width, alignment, copy, false, inc_n_data, false, roc_ops); + quadsort_direct(array, len, cmp, cmp_data, element_width, alignment, copy, false, inc_n_context, inc_n_data, false, roc_ops); } } else { const alloc_ptr = roc_ops.alloc(len * @sizeOf(usize), @alignOf(usize)); // Build list of pointers to sort. - const arr_ptr = @as([*]Opaque, @ptrCast(@alignCast(alloc_ptr))); + const arr_ptr: [*]Opaque = utils.alignedPtrCast([*]Opaque, @as([*]u8, @ptrCast(alloc_ptr)), @src()); defer roc_ops.dealloc(alloc_ptr, @alignOf(usize)); for (0..len) |i| { arr_ptr[i] = array + i * element_width; @@ -914,9 +928,9 @@ pub fn quadsort( // Sort. if (data_is_owned_runtime) { - quadsort_direct(@ptrCast(arr_ptr), len, cmp, cmp_data, @sizeOf(usize), @alignOf(usize), &pointer_copy, true, inc_n_data, true, roc_ops); + quadsort_direct(@ptrCast(arr_ptr), len, cmp, cmp_data, @sizeOf(usize), @alignOf(usize), &pointer_copy, true, inc_n_context, inc_n_data, true, roc_ops); } else { - quadsort_direct(@ptrCast(arr_ptr), len, cmp, cmp_data, @sizeOf(usize), @alignOf(usize), &pointer_copy, false, inc_n_data, true, roc_ops); + quadsort_direct(@ptrCast(arr_ptr), len, cmp, cmp_data, @sizeOf(usize), @alignOf(usize), &pointer_copy, false, inc_n_context, inc_n_data, true, roc_ops); } const collect_ptr = roc_ops.alloc(len * element_width, alignment); @@ -940,6 +954,7 @@ fn quadsort_direct( alignment: u32, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, roc_ops: *RocOps, @@ -952,15 +967,15 @@ fn quadsort_direct( // Also, zig doesn't have alloca, so we always do max size here. var swap_buffer: [MAX_ELEMENT_BUFFER_SIZE * 32]u8 align(BufferAlign) = undefined; const swap = @as([*]u8, @ptrCast(&swap_buffer[0])); - tail_swap(arr_ptr, len, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - } else if (quad_swap(arr_ptr, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect) != .sorted) { + tail_swap(arr_ptr, len, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + } else if (quad_swap(arr_ptr, len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect) != .sorted) { const swap_len = len; const swap = roc_ops.alloc(swap_len * element_width, alignment); - const block_len = quad_merge(arr_ptr, len, @ptrCast(swap), swap_len, 32, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + const block_len = quad_merge(arr_ptr, len, @ptrCast(swap), swap_len, 32, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); - rotate_merge(arr_ptr, len, @ptrCast(swap), swap_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + rotate_merge(arr_ptr, len, @ptrCast(swap), swap_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); roc_ops.dealloc(swap, alignment); } @@ -972,6 +987,7 @@ fn quadsort_stack_swap( cmp: CompareFn, cmp_data: Opaque, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, element_width: usize, copy: CopyFn, @@ -981,12 +997,12 @@ fn quadsort_stack_swap( var swap_buffer: [MAX_ELEMENT_BUFFER_SIZE * 512]u8 align(BufferAlign) = undefined; const swap = @as([*]u8, @ptrCast(&swap_buffer[0])); - const block_len = quad_merge(array, len, swap, 512, 32, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + const block_len = quad_merge(array, len, swap, 512, 32, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); - rotate_merge(array, len, swap, 512, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + rotate_merge(array, len, swap, 512, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } -// ================ Inplace Rotate Merge ====================================== +// Inplace Rotate Merge // These are used as backup if the swap size is not large enough. // Also can be used for the final merge to reduce memory footprint. @@ -1002,13 +1018,14 @@ pub fn rotate_merge( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { const end_ptr = array + len * element_width; if (len <= block_len * 2 and len -% block_len <= swap_len) { - partial_backwards_merge(array, len, swap, swap_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + partial_backwards_merge(array, len, swap, swap_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); return; } @@ -1017,11 +1034,11 @@ pub fn rotate_merge( var arr_ptr = array; while (@intFromPtr(arr_ptr) + current_block_len * element_width < @intFromPtr(end_ptr)) : (arr_ptr += current_block_len * 2 * element_width) { if (@intFromPtr(arr_ptr) + current_block_len * 2 * element_width < @intFromPtr(end_ptr)) { - rotate_merge_block(arr_ptr, swap, swap_len, current_block_len, current_block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + rotate_merge_block(arr_ptr, swap, swap_len, current_block_len, current_block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); continue; } const right_len = (@intFromPtr(end_ptr) - @intFromPtr(arr_ptr)) / element_width - current_block_len; - rotate_merge_block(arr_ptr, swap, swap_len, current_block_len, right_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + rotate_merge_block(arr_ptr, swap, swap_len, current_block_len, right_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); break; } } @@ -1039,6 +1056,7 @@ fn rotate_merge_block( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { @@ -1046,7 +1064,7 @@ fn rotate_merge_block( var right = initial_right; // 1 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 1); + inc_n_data(inc_n_context, cmp_data, 1); } if (compare(cmp, cmp_data, array + (left_block - 1) * element_width, array + left_block * element_width, indirect) != GT) { // Lucky us, already sorted. @@ -1056,7 +1074,7 @@ fn rotate_merge_block( const right_block = left_block / 2; left_block -= right_block; - const left = monobound_binary_first(array + (left_block + right_block) * element_width, right, array + left_block * element_width, cmp, cmp_data, element_width, data_is_owned, inc_n_data, indirect); + const left = monobound_binary_first(array + (left_block + right_block) * element_width, right, array + left_block * element_width, cmp, cmp_data, element_width, data_is_owned, inc_n_context, inc_n_data, indirect); right -= left; if (left != 0) { @@ -1065,17 +1083,17 @@ fn rotate_merge_block( @memcpy((swap + left_block * element_width)[0..(left * element_width)], (array + (left_block + right_block) * element_width)[0..(left * element_width)]); std.mem.copyBackwards(u8, (array + (left + left_block) * element_width)[0..(right_block * element_width)], (array + left_block * element_width)[0..(right_block * element_width)]); - cross_merge(array, swap, left_block, left, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + cross_merge(array, swap, left_block, left, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } else { trinity_rotation(array + left_block * element_width, right_block + left, swap, swap_len, right_block, element_width, copy); const unbalanced = (left * 2 < left_block) or (left_block * 2 < left); if (unbalanced and left <= swap_len) { - partial_backwards_merge(array, left_block + left, swap, swap_len, left_block, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + partial_backwards_merge(array, left_block + left, swap, swap_len, left_block, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } else if (unbalanced and left_block <= swap_len) { - partial_forward_merge(array, left_block + left, swap, swap_len, left_block, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + partial_forward_merge(array, left_block + left, swap, swap_len, left_block, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } else { - rotate_merge_block(array, swap, swap_len, left_block, left, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + rotate_merge_block(array, swap, swap_len, left_block, left, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } } } @@ -1083,11 +1101,11 @@ fn rotate_merge_block( if (right != 0) { const unbalanced = (right * 2 < right_block) or (right_block * 2 < right); if ((unbalanced and right <= swap_len) or right + right_block <= swap_len) { - partial_backwards_merge(array + (left_block + left) * element_width, right_block + right, swap, swap_len, right_block, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + partial_backwards_merge(array + (left_block + left) * element_width, right_block + right, swap, swap_len, right_block, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } else if (unbalanced and left_block <= swap_len) { - partial_forward_merge(array + (left_block + left) * element_width, right_block + right, swap, swap_len, right_block, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + partial_forward_merge(array + (left_block + left) * element_width, right_block + right, swap, swap_len, right_block, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } else { - rotate_merge_block(array + (left_block + left) * element_width, swap, swap_len, right_block, right, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + rotate_merge_block(array + (left_block + left) * element_width, swap, swap_len, right_block, right, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } } } @@ -1101,6 +1119,7 @@ pub fn monobound_binary_first( cmp_data: Opaque, element_width: usize, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) usize { @@ -1113,7 +1132,7 @@ pub fn monobound_binary_first( // Needs to be `-1` so values that are powers of 2 don't sort up a bin. // Then just add 1 back to the final result. const log2 = @bitSizeOf(usize) - @clz(top - 1) + 1; - inc_n_data(cmp_data, log2); + inc_n_data(inc_n_context, cmp_data, log2); } while (top > 1) { const mid = top / 2; @@ -1290,7 +1309,7 @@ pub fn trinity_rotation( } } -// ================ Unbalanced Merges ========================================= +// Unbalanced Merges /// Merges the remaining blocks at the tail of the array. pub fn tail_merge( @@ -1304,6 +1323,7 @@ pub fn tail_merge( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { @@ -1313,11 +1333,11 @@ pub fn tail_merge( var arr_ptr = array; while (@intFromPtr(arr_ptr) + current_block_len * element_width < @intFromPtr(end_ptr)) : (arr_ptr += 2 * current_block_len * element_width) { if (@intFromPtr(arr_ptr) + 2 * current_block_len * element_width < @intFromPtr(end_ptr)) { - partial_backwards_merge(arr_ptr, 2 * current_block_len, swap, swap_len, current_block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + partial_backwards_merge(arr_ptr, 2 * current_block_len, swap, swap_len, current_block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); continue; } const rem_len = (@intFromPtr(end_ptr) - @intFromPtr(arr_ptr)) / element_width; - partial_backwards_merge(arr_ptr, rem_len, swap, swap_len, current_block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + partial_backwards_merge(arr_ptr, rem_len, swap, swap_len, current_block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); break; } } @@ -1336,6 +1356,7 @@ pub fn partial_backwards_merge( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { @@ -1351,7 +1372,7 @@ pub fn partial_backwards_merge( // 1 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 1); + inc_n_data(inc_n_context, cmp_data, 1); } if (compare(cmp, cmp_data, left_tail, left_tail + element_width, indirect) != GT) { // Lucky case, blocks happen to be sorted. @@ -1362,7 +1383,7 @@ pub fn partial_backwards_merge( if (len <= swap_len and right_len >= 64) { // Large remaining merge and we have enough space to just do it in swap. - cross_merge(swap, array, block_len, right_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + cross_merge(swap, array, block_len, right_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); @memcpy(array[0..(element_width * len)], swap[0..(element_width * len)]); @@ -1376,7 +1397,7 @@ pub fn partial_backwards_merge( // For backwards, we first try to do really large chunks, of 16 elements. outer: while (@intFromPtr(left_tail) > @intFromPtr(array + 16 * element_width) and @intFromPtr(right_tail) > @intFromPtr(swap + 16 * element_width)) { // Due to if looping, these must use `compare_inc` - while (compare_inc(cmp, cmp_data, left_tail, right_tail - 15 * element_width, data_is_owned, inc_n_data, indirect) != GT) { + while (compare_inc(cmp, cmp_data, left_tail, right_tail - 15 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { inline for (0..16) |_| { copy(dest_tail, right_tail); dest_tail -= element_width; @@ -1386,7 +1407,7 @@ pub fn partial_backwards_merge( break :outer; } // Due to if looping, these must use `compare_inc` - while (compare_inc(cmp, cmp_data, left_tail - 15 * element_width, right_tail, data_is_owned, inc_n_data, indirect) == GT) { + while (compare_inc(cmp, cmp_data, left_tail - 15 * element_width, right_tail, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { inline for (0..16) |_| { copy(dest_tail, left_tail); dest_tail -= element_width; @@ -1399,13 +1420,13 @@ pub fn partial_backwards_merge( var loops: usize = 8; while (true) { // Due to if else chain and uncertain calling, these must use `compare_inc` - if (compare_inc(cmp, cmp_data, left_tail, right_tail - element_width, data_is_owned, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, left_tail, right_tail - element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { inline for (0..2) |_| { copy(dest_tail, right_tail); dest_tail -= element_width; right_tail -= element_width; } - } else if (compare_inc(cmp, cmp_data, left_tail - element_width, right_tail, data_is_owned, inc_n_data, indirect) == GT) { + } else if (compare_inc(cmp, cmp_data, left_tail - element_width, right_tail, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { inline for (0..2) |_| { copy(dest_tail, left_tail); dest_tail -= element_width; @@ -1415,7 +1436,7 @@ pub fn partial_backwards_merge( // Couldn't move two elements, do a cross swap and continue. // 2 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 2); + inc_n_data(inc_n_context, cmp_data, 2); } const lte = compare(cmp, cmp_data, left_tail, right_tail, indirect) != GT; const x = if (lte) element_width else 0; @@ -1443,13 +1464,13 @@ pub fn partial_backwards_merge( // The C use `goto` to implement the two tail recursive functions below inline. // I think the closest equivalent in zig would be to use an enum and a switch. // That would potentially optimize to computed gotos. - const break_loop = partial_forward_merge_right_tail_2(&dest_tail, &array, &left_tail, &swap, &right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + const break_loop = partial_forward_merge_right_tail_2(&dest_tail, &array, &left_tail, &swap, &right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); if (break_loop) break; // 2 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 2); + inc_n_data(inc_n_context, cmp_data, 2); } // Couldn't move two elements, do a cross swap and continue. const lte = compare(cmp, cmp_data, left_tail, right_tail, indirect) != GT; @@ -1470,7 +1491,7 @@ pub fn partial_backwards_merge( // This feels like a place where we may be able reduce inc_n_data calls. // 1 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 1); + inc_n_data(inc_n_context, cmp_data, 1); } tail_branchless_merge(&dest_tail, &left_tail, &right_tail, cmp, cmp_data, element_width, copy, indirect); } @@ -1495,28 +1516,29 @@ fn partial_forward_merge_right_tail_2( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) bool { - if (compare_inc(cmp, cmp_data, left_tail.*, right_tail.* - element_width, data_is_owned, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, left_tail.*, right_tail.* - element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { inline for (0..2) |_| { copy(dest.*, right_tail.*); dest.* -= element_width; right_tail.* -= element_width; } if (@intFromPtr(right_tail.*) > @intFromPtr(right_head.*) + element_width) { - return partial_forward_merge_right_tail_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + return partial_forward_merge_right_tail_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } return true; } - if (compare_inc(cmp, cmp_data, left_tail.* - element_width, right_tail.*, data_is_owned, inc_n_data, indirect) == GT) { + if (compare_inc(cmp, cmp_data, left_tail.* - element_width, right_tail.*, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { inline for (0..2) |_| { copy(dest.*, left_tail.*); dest.* -= element_width; left_tail.* -= element_width; } if (@intFromPtr(left_tail.*) > @intFromPtr(left_head.*) + element_width) { - return partial_forward_merge_left_tail_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + return partial_forward_merge_left_tail_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } return true; } @@ -1534,28 +1556,29 @@ fn partial_forward_merge_left_tail_2( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) bool { - if (compare_inc(cmp, cmp_data, left_tail.* - element_width, right_tail.*, data_is_owned, inc_n_data, indirect) == GT) { + if (compare_inc(cmp, cmp_data, left_tail.* - element_width, right_tail.*, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { inline for (0..2) |_| { copy(dest.*, left_tail.*); dest.* -= element_width; left_tail.* -= element_width; } if (@intFromPtr(left_tail.*) > @intFromPtr(left_head.*) + element_width) { - return partial_forward_merge_left_tail_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + return partial_forward_merge_left_tail_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } return true; } - if (compare_inc(cmp, cmp_data, left_tail.*, right_tail.* - element_width, data_is_owned, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, left_tail.*, right_tail.* - element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { inline for (0..2) |_| { copy(dest.*, right_tail.*); dest.* -= element_width; right_tail.* -= element_width; } if (@intFromPtr(right_tail.*) > @intFromPtr(right_head.*) + element_width) { - return partial_forward_merge_right_tail_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + return partial_forward_merge_right_tail_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } return true; } @@ -1575,6 +1598,7 @@ pub fn partial_forward_merge( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { @@ -1590,7 +1614,7 @@ pub fn partial_forward_merge( // 1 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 1); + inc_n_data(inc_n_context, cmp_data, 1); } if (compare(cmp, cmp_data, right_head - element_width, right_head, indirect) != GT) { // Lucky case, blocks happen to be sorted. @@ -1610,13 +1634,13 @@ pub fn partial_forward_merge( // The C use `goto` to implement the two tail recursive functions below inline. // I think the closest equivalent in zig would be to use an enum and a switch. // That would potentially optimize to computed gotos. - const break_loop = partial_forward_merge_right_head_2(&dest_head, &left_head, &left_tail, &right_head, &right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + const break_loop = partial_forward_merge_right_head_2(&dest_head, &left_head, &left_tail, &right_head, &right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); if (break_loop) break; // 2 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 2); + inc_n_data(inc_n_context, cmp_data, 2); } // Couldn't move two elements, do a cross swap and continue. const lte = compare(cmp, cmp_data, left_head, right_head, indirect) != GT; @@ -1636,7 +1660,7 @@ pub fn partial_forward_merge( // This feels like a place where we may be able reduce inc_n_data calls. // 1 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 1); + inc_n_data(inc_n_context, cmp_data, 1); } head_branchless_merge(&dest_head, &left_head, &right_head, cmp, cmp_data, element_width, copy, indirect); } @@ -1661,28 +1685,29 @@ fn partial_forward_merge_right_head_2( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) bool { - if (compare_inc(cmp, cmp_data, left_head.*, right_head.* + element_width, data_is_owned, inc_n_data, indirect) == GT) { + if (compare_inc(cmp, cmp_data, left_head.*, right_head.* + element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { inline for (0..2) |_| { copy(dest.*, right_head.*); dest.* += element_width; right_head.* += element_width; } if (@intFromPtr(right_head.*) < @intFromPtr(right_tail.*) - element_width) { - return partial_forward_merge_right_head_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + return partial_forward_merge_right_head_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } return true; } - if (compare_inc(cmp, cmp_data, left_head.* + element_width, right_head.*, data_is_owned, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, left_head.* + element_width, right_head.*, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { inline for (0..2) |_| { copy(dest.*, left_head.*); dest.* += element_width; left_head.* += element_width; } if (@intFromPtr(left_head.*) < @intFromPtr(left_tail.*) - element_width) { - return partial_forward_merge_left_head_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + return partial_forward_merge_left_head_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } return true; } @@ -1700,35 +1725,36 @@ fn partial_forward_merge_left_head_2( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) bool { - if (compare_inc(cmp, cmp_data, left_head.* + element_width, right_head.*, data_is_owned, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, left_head.* + element_width, right_head.*, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { inline for (0..2) |_| { copy(dest.*, left_head.*); dest.* += element_width; left_head.* += element_width; } if (@intFromPtr(left_head.*) < @intFromPtr(left_tail.*) - element_width) { - return partial_forward_merge_left_head_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + return partial_forward_merge_left_head_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } return true; } - if (compare_inc(cmp, cmp_data, left_head.*, right_head.* + element_width, data_is_owned, inc_n_data, indirect) == GT) { + if (compare_inc(cmp, cmp_data, left_head.*, right_head.* + element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { inline for (0..2) |_| { copy(dest.*, right_head.*); dest.* += element_width; right_head.* += element_width; } if (@intFromPtr(right_head.*) < @intFromPtr(right_tail.*) - element_width) { - return partial_forward_merge_right_head_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + return partial_forward_merge_right_head_2(dest, left_head, left_tail, right_head, right_tail, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } return true; } return false; } -// ================ Quad Merge Support ======================================== +// Quad Merge Support /// Merges an array of of sized blocks of sorted elements with a tail. /// Returns the block length of sorted runs after the call. @@ -1744,6 +1770,7 @@ pub fn quad_merge( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) usize { @@ -1753,7 +1780,7 @@ pub fn quad_merge( while (current_block_len <= len and current_block_len <= swap_len) : (current_block_len *= 4) { var arr_ptr = array; while (true) { - quad_merge_block(arr_ptr, swap, current_block_len / 4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + quad_merge_block(arr_ptr, swap, current_block_len / 4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); arr_ptr += current_block_len * element_width; if (@intFromPtr(arr_ptr) + current_block_len * element_width > @intFromPtr(end_ptr)) @@ -1761,10 +1788,10 @@ pub fn quad_merge( } const rem_len = (@intFromPtr(end_ptr) - @intFromPtr(arr_ptr)) / element_width; - tail_merge(arr_ptr, rem_len, swap, swap_len, current_block_len / 4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + tail_merge(arr_ptr, rem_len, swap, swap_len, current_block_len / 4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } - tail_merge(array, len, swap, swap_len, current_block_len / 4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + tail_merge(array, len, swap, swap_len, current_block_len / 4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); return current_block_len / 2; } @@ -1779,6 +1806,7 @@ pub fn quad_merge_block( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { @@ -1791,7 +1819,7 @@ pub fn quad_merge_block( // 2 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 2); + inc_n_data(inc_n_context, cmp_data, 2); } const in_order_1_2: u2 = @intFromBool(compare(cmp, cmp_data, block2 - element_width, block2, indirect) != GT); const in_order_3_4: u2 = @intFromBool(compare(cmp, cmp_data, block4 - element_width, block4, indirect) != GT); @@ -1799,23 +1827,23 @@ pub fn quad_merge_block( switch (in_order_1_2 | (in_order_3_4 << 1)) { 0 => { // Nothing sorted. Just run merges on both. - cross_merge(swap, array, block_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - cross_merge(swap + block_x_2 * element_width, block3, block_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + cross_merge(swap, array, block_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + cross_merge(swap + block_x_2 * element_width, block3, block_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, 1 => { // First half sorted already. @memcpy(swap[0..(element_width * block_x_2)], array[0..(element_width * block_x_2)]); - cross_merge(swap + block_x_2 * element_width, block3, block_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + cross_merge(swap + block_x_2 * element_width, block3, block_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, 2 => { // Second half sorted already. - cross_merge(swap, array, block_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + cross_merge(swap, array, block_len, block_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); @memcpy((swap + element_width * block_x_2)[0..(element_width * block_x_2)], block3[0..(element_width * block_x_2)]); }, 3 => { // 1 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 1); + inc_n_data(inc_n_context, cmp_data, 1); } const in_order_2_3 = compare(cmp, cmp_data, block3 - element_width, block3, indirect) != GT; if (in_order_2_3) @@ -1828,7 +1856,7 @@ pub fn quad_merge_block( } // Merge 2 larger blocks. - cross_merge(array, swap, block_x_2, block_x_2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + cross_merge(array, swap, block_x_2, block_x_2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } /// Cross merge attempts to merge two arrays in chunks of multiple elements. @@ -1842,6 +1870,7 @@ pub fn cross_merge( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { @@ -1855,8 +1884,8 @@ pub fn cross_merge( if (left_len + 1 >= right_len and right_len + 1 >= left_len and left_len >= 32) { const offset = 15 * element_width; // Due to short circuit logic, these must use `compare_inc` - if (compare_inc(cmp, cmp_data, left_head + offset, right_head, data_is_owned, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, left_head, right_head + offset, data_is_owned, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, left_tail, right_tail - offset, data_is_owned, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, left_tail - offset, right_tail, data_is_owned, inc_n_data, indirect) != GT) { - parity_merge(dest, src, left_len, right_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + if (compare_inc(cmp, cmp_data, left_head + offset, right_head, data_is_owned, inc_n_context, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, left_head, right_head + offset, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, left_tail, right_tail - offset, data_is_owned, inc_n_context, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, left_tail - offset, right_tail, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { + parity_merge(dest, src, left_len, right_len, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); return; } } @@ -1869,7 +1898,7 @@ pub fn cross_merge( if (@as(isize, @intCast(@intFromPtr(left_tail))) - @as(isize, @intCast(@intFromPtr(left_head))) > @as(isize, @intCast(8 * element_width))) { // 8 elements all less than or equal to and can be moved together. // Due to looping, these must use `compare_inc` - while (compare_inc(cmp, cmp_data, left_head + 7 * element_width, right_head, data_is_owned, inc_n_data, indirect) != GT) { + while (compare_inc(cmp, cmp_data, left_head + 7 * element_width, right_head, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { inline for (0..8) |_| { copy(dest_head, left_head); dest_head += element_width; @@ -1882,7 +1911,7 @@ pub fn cross_merge( // Attempt to do the same from the tail. // 8 elements all greater than and can be moved together. // Due to looping, these must use `compare_inc` - while (compare_inc(cmp, cmp_data, left_tail - 7 * element_width, right_tail, data_is_owned, inc_n_data, indirect) == GT) { + while (compare_inc(cmp, cmp_data, left_tail - 7 * element_width, right_tail, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { inline for (0..8) |_| { copy(dest_tail, left_tail); dest_tail -= element_width; @@ -1898,7 +1927,7 @@ pub fn cross_merge( if (@as(isize, @intCast(@intFromPtr(right_tail))) - @as(isize, @intCast(@intFromPtr(right_head))) > @as(isize, @intCast(8 * element_width))) { // left greater than 8 elements right and can be moved together. // Due to looping, these must use `compare_inc` - while (compare_inc(cmp, cmp_data, left_head, right_head + 7 * element_width, data_is_owned, inc_n_data, indirect) == GT) { + while (compare_inc(cmp, cmp_data, left_head, right_head + 7 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { inline for (0..8) |_| { copy(dest_head, right_head); dest_head += element_width; @@ -1911,7 +1940,7 @@ pub fn cross_merge( // Attempt to do the same from the tail. // left less than or equalt to 8 elements right and can be moved together. // Due to looping, these must use `compare_inc` - while (compare_inc(cmp, cmp_data, left_tail, right_tail - 7 * element_width, data_is_owned, inc_n_data, indirect) != GT) { + while (compare_inc(cmp, cmp_data, left_tail, right_tail - 7 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { inline for (0..8) |_| { copy(dest_tail, right_tail); dest_tail -= element_width; @@ -1928,7 +1957,7 @@ pub fn cross_merge( // Large enough to warrant a two way merge. // 16 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 16); + inc_n_data(inc_n_context, cmp_data, 16); } for (0..8) |_| { head_branchless_merge(&dest_head, &left_head, &right_head, cmp, cmp_data, element_width, copy, indirect); @@ -1941,7 +1970,7 @@ pub fn cross_merge( // This feels like a place where we may be able reduce inc_n_data calls. // 1 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 1); + inc_n_data(inc_n_context, cmp_data, 1); } head_branchless_merge(&dest_head, &left_head, &right_head, cmp, cmp_data, element_width, copy, indirect); } @@ -1957,7 +1986,7 @@ pub fn cross_merge( } } -// ================ 32 Element Blocks ========================================= +// 32 Element Blocks const QuadSwapResult = enum { sorted, @@ -1973,6 +2002,7 @@ pub fn quad_swap( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) QuadSwapResult { @@ -1995,7 +2025,7 @@ pub fn quad_swap( // 4 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 4); + inc_n_data(inc_n_context, cmp_data, 4); } var v1: u4 = @intFromBool(compare(cmp, cmp_data, arr_ptr + 0 * element_width, arr_ptr + 1 * element_width, indirect) == GT); var v2: u4 = @intFromBool(compare(cmp, cmp_data, arr_ptr + 2 * element_width, arr_ptr + 3 * element_width, indirect) == GT); @@ -2010,12 +2040,12 @@ pub fn quad_swap( 0 => { // potentially already ordered, check rest! // Due to short circuit logic, these must use `compare_inc` - if (compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { break :switch_state .ordered; } // 16 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 16); + inc_n_data(inc_n_context, cmp_data, 16); } quad_swap_merge(arr_ptr, swap, cmp, cmp_data, element_width, copy, indirect); @@ -2025,7 +2055,7 @@ pub fn quad_swap( 15 => { // potentially already reverse ordered, check rest! // Due to short circuit logic, these must use `compare_inc` - if (compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_data, indirect) == GT) { + if (compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { reverse_head = arr_ptr; break :switch_state .reversed; } @@ -2051,7 +2081,7 @@ pub fn quad_swap( // 16 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 16); + inc_n_data(inc_n_context, cmp_data, 16); } quad_swap_merge(arr_ptr, swap, cmp, cmp_data, element_width, copy, indirect); @@ -2066,7 +2096,7 @@ pub fn quad_swap( count -= 1; // 4 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 4); + inc_n_data(inc_n_context, cmp_data, 4); } v1 = @intFromBool(compare(cmp, cmp_data, arr_ptr + 0 * element_width, arr_ptr + 1 * element_width, indirect) == GT); v2 = @intFromBool(compare(cmp, cmp_data, arr_ptr + 2 * element_width, arr_ptr + 3 * element_width, indirect) == GT); @@ -2075,7 +2105,7 @@ pub fn quad_swap( if (v1 | v2 | v3 | v4 != 0) { // Sadly not ordered still, maybe reversed though? // Due to short circuit logic, these must use `compare_inc` - if (v1 + v2 + v3 + v4 == 4 and compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_data, indirect) == GT) { + if (v1 + v2 + v3 + v4 == 4 and compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { reverse_head = arr_ptr; state = .reversed; continue; @@ -2084,14 +2114,14 @@ pub fn quad_swap( continue; } // Due to short circuit logic, these must use `compare_inc` - if (compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { state = .ordered; continue; } // 16 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 16); + inc_n_data(inc_n_context, cmp_data, 16); } quad_swap_merge(arr_ptr, swap, cmp, cmp_data, element_width, copy, indirect); arr_ptr += 8 * element_width; @@ -2107,7 +2137,7 @@ pub fn quad_swap( count -= 1; // 4 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 4); + inc_n_data(inc_n_context, cmp_data, 4); } v1 = @intFromBool(compare(cmp, cmp_data, arr_ptr + 0 * element_width, arr_ptr + 1 * element_width, indirect) != GT); v2 = @intFromBool(compare(cmp, cmp_data, arr_ptr + 2 * element_width, arr_ptr + 3 * element_width, indirect) != GT); @@ -2119,7 +2149,7 @@ pub fn quad_swap( } else { // This also checks the boundary between this and the last block. // Due to short circuit logic, these must use `compare_inc` - if (compare_inc(cmp, cmp_data, arr_ptr - 1 * element_width, arr_ptr + 0 * element_width, data_is_owned, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_data, indirect) == GT) { + if (compare_inc(cmp, cmp_data, arr_ptr - 1 * element_width, arr_ptr + 0 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { // Row multiple reversed blocks in a row! state = .reversed; continue; @@ -2130,12 +2160,12 @@ pub fn quad_swap( // Since we already have v1 to v4, check the next block state. // Due to short circuit logic, these must use `compare_inc` - if (v1 + v2 + v3 + v4 == 4 and compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_data, indirect) != GT) { + if (v1 + v2 + v3 + v4 == 4 and compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { state = .ordered; continue; } // Due to short circuit logic, these must use `compare_inc` - if (v1 + v2 + v3 + v4 == 0 and compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_data, indirect) == GT) { + if (v1 + v2 + v3 + v4 == 0 and compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { reverse_head = arr_ptr; state = .reversed; continue; @@ -2153,10 +2183,10 @@ pub fn quad_swap( arr_ptr -= 8 * element_width; // Due to short circuit logic, these must use `compare_inc` - if (compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_data, indirect) == GT or compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_data, indirect) == GT or compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_data, indirect) == GT) { + if (compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT or compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT or compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) == GT) { // 16 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 16); + inc_n_data(inc_n_context, cmp_data, 16); } quad_swap_merge(arr_ptr, swap, cmp, cmp_data, element_width, copy, indirect); } @@ -2168,19 +2198,19 @@ pub fn quad_swap( const rem = len % 8; reverse_block: { // Due to chance of breaking and not running, must use `compare_inc`. - if (rem == 7 and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_data, indirect) != GT) + if (rem == 7 and compare_inc(cmp, cmp_data, arr_ptr + 5 * element_width, arr_ptr + 6 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) break :reverse_block; - if (rem >= 6 and compare_inc(cmp, cmp_data, arr_ptr + 4 * element_width, arr_ptr + 5 * element_width, data_is_owned, inc_n_data, indirect) != GT) + if (rem >= 6 and compare_inc(cmp, cmp_data, arr_ptr + 4 * element_width, arr_ptr + 5 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) break :reverse_block; - if (rem >= 5 and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_data, indirect) != GT) + if (rem >= 5 and compare_inc(cmp, cmp_data, arr_ptr + 3 * element_width, arr_ptr + 4 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) break :reverse_block; - if (rem >= 4 and compare_inc(cmp, cmp_data, arr_ptr + 2 * element_width, arr_ptr + 3 * element_width, data_is_owned, inc_n_data, indirect) != GT) + if (rem >= 4 and compare_inc(cmp, cmp_data, arr_ptr + 2 * element_width, arr_ptr + 3 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) break :reverse_block; - if (rem >= 3 and compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_data, indirect) != GT) + if (rem >= 3 and compare_inc(cmp, cmp_data, arr_ptr + 1 * element_width, arr_ptr + 2 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) break :reverse_block; - if (rem >= 2 and compare_inc(cmp, cmp_data, arr_ptr + 0 * element_width, arr_ptr + 1 * element_width, data_is_owned, inc_n_data, indirect) != GT) + if (rem >= 2 and compare_inc(cmp, cmp_data, arr_ptr + 0 * element_width, arr_ptr + 1 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) break :reverse_block; - if (rem >= 1 and compare_inc(cmp, cmp_data, arr_ptr - 1 * element_width, arr_ptr + 0 * element_width, data_is_owned, inc_n_data, indirect) != GT) + if (rem >= 1 and compare_inc(cmp, cmp_data, arr_ptr - 1 * element_width, arr_ptr + 0 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) break :reverse_block; quad_reversal(reverse_head, arr_ptr + rem * element_width - element_width, element_width, copy); @@ -2199,7 +2229,7 @@ pub fn quad_swap( } } if (!skip_tail_swap) { - tail_swap(arr_ptr, len % 8, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + tail_swap(arr_ptr, len % 8, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } // Group into 32 element blocks. @@ -2211,19 +2241,19 @@ pub fn quad_swap( arr_ptr += 32 * element_width; }) { // Due to short circuit logic, these must use `compare_inc` - if (compare_inc(cmp, cmp_data, arr_ptr + 7 * element_width, arr_ptr + 8 * element_width, data_is_owned, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 15 * element_width, arr_ptr + 16 * element_width, data_is_owned, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 23 * element_width, arr_ptr + 24 * element_width, data_is_owned, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, arr_ptr + 7 * element_width, arr_ptr + 8 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 15 * element_width, arr_ptr + 16 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr + 23 * element_width, arr_ptr + 24 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { // Already in order. continue; } - parity_merge(swap, arr_ptr, 8, 8, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - parity_merge(swap + 16 * element_width, arr_ptr + 16 * element_width, 8, 8, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - parity_merge(arr_ptr, swap, 16, 16, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + parity_merge(swap, arr_ptr, 8, 8, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + parity_merge(swap + 16 * element_width, arr_ptr + 16 * element_width, 8, 8, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + parity_merge(arr_ptr, swap, 16, 16, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } // Deal with final tail for 32 element blocks. // Anything over 8 elements is multiple blocks worth merging together. if (len % 32 > 8) { - tail_merge(arr_ptr, len % 32, swap, 32, 8, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + tail_merge(arr_ptr, len % 32, swap, 32, 8, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } return .unfinished; @@ -2296,7 +2326,7 @@ pub fn quad_reversal( } } -// ================ Small Arrays ============================================== +// Small Arrays // Below are functions for sorting under 32 element arrays. /// Uses swap space to sort the tail of an array. @@ -2310,11 +2340,12 @@ pub fn tail_swap( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { if (len < 8) { - tiny_sort(array, len, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + tiny_sort(array, len, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); return; } @@ -2326,22 +2357,22 @@ pub fn tail_swap( const quad4 = half2 - quad3; var arr_ptr = array; - tail_swap(arr_ptr, quad1, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + tail_swap(arr_ptr, quad1, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); arr_ptr += quad1 * element_width; - tail_swap(arr_ptr, quad2, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + tail_swap(arr_ptr, quad2, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); arr_ptr += quad2 * element_width; - tail_swap(arr_ptr, quad3, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + tail_swap(arr_ptr, quad3, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); arr_ptr += quad3 * element_width; - tail_swap(arr_ptr, quad4, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + tail_swap(arr_ptr, quad4, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); // Due to short circuit logic, these must use `compare_inc` - if (compare_inc(cmp, cmp_data, array + (quad1 - 1) * element_width, array + quad1 * element_width, data_is_owned, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, array + (half1 - 1) * element_width, array + half1 * element_width, data_is_owned, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr - 1 * element_width, arr_ptr, data_is_owned, inc_n_data, indirect) != GT) { + if (compare_inc(cmp, cmp_data, array + (quad1 - 1) * element_width, array + quad1 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, array + (half1 - 1) * element_width, array + half1 * element_width, data_is_owned, inc_n_context, inc_n_data, indirect) != GT and compare_inc(cmp, cmp_data, arr_ptr - 1 * element_width, arr_ptr, data_is_owned, inc_n_context, inc_n_data, indirect) != GT) { return; } - parity_merge(swap, array, quad1, quad2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - parity_merge(swap + half1 * element_width, array + half1 * element_width, quad3, quad4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); - parity_merge(array, swap, half1, half2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + parity_merge(swap, array, quad1, quad2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + parity_merge(swap + half1 * element_width, array + half1 * element_width, quad3, quad4, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); + parity_merge(array, swap, half1, half2, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); } /// Merges two neighboring sorted arrays into dest. @@ -2356,6 +2387,7 @@ pub fn parity_merge( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { @@ -2372,14 +2404,14 @@ pub fn parity_merge( if (left_len < right_len) { // 1 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 1); + inc_n_data(inc_n_context, cmp_data, 1); } head_branchless_merge(&dest_head, &left_head, &right_head, cmp, cmp_data, element_width, copy, indirect); } // 2 + 2(left_len -1) = (2*left_len) guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 2 * left_len); + inc_n_data(inc_n_context, cmp_data, 2 * left_len); } head_branchless_merge(&dest_head, &left_head, &right_head, cmp, cmp_data, element_width, copy, indirect); @@ -2390,7 +2422,7 @@ pub fn parity_merge( tail_branchless_merge(&dest_tail, &left_tail, &right_tail, cmp, cmp_data, element_width, copy, indirect); } -// ================ Tiny Arrays =============================================== +// Tiny Arrays // Below are functions for sorting 0 to 7 element arrays. /// Sort arrays of 0 to 7 elements. @@ -2403,6 +2435,7 @@ pub fn tiny_sort( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { @@ -2418,14 +2451,14 @@ pub fn tiny_sort( 2 => { // 1 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 1); + inc_n_data(inc_n_context, cmp_data, 1); } swap_branchless(array, tmp_ptr, cmp, cmp_data, element_width, copy, indirect); }, 3 => { // 3 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 3); + inc_n_data(inc_n_context, cmp_data, 3); } var arr_ptr = array; swap_branchless(arr_ptr, tmp_ptr, cmp, cmp_data, element_width, copy, indirect); @@ -2435,16 +2468,16 @@ pub fn tiny_sort( swap_branchless(arr_ptr, tmp_ptr, cmp, cmp_data, element_width, copy, indirect); }, 4 => { - parity_swap_four(array, tmp_ptr, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + parity_swap_four(array, tmp_ptr, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, 5 => { - parity_swap_five(array, tmp_ptr, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + parity_swap_five(array, tmp_ptr, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, 6 => { - parity_swap_six(array, tmp_ptr, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + parity_swap_six(array, tmp_ptr, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, 7 => { - parity_swap_seven(array, tmp_ptr, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_data, indirect); + parity_swap_seven(array, tmp_ptr, swap, cmp, cmp_data, element_width, copy, data_is_owned, inc_n_context, inc_n_data, indirect); }, else => { unreachable; @@ -2460,12 +2493,13 @@ fn parity_swap_four( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { // 3 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 3); + inc_n_data(inc_n_context, cmp_data, 3); } var arr_ptr = array; swap_branchless(arr_ptr, tmp_ptr, cmp, cmp_data, element_width, copy, indirect); @@ -2477,7 +2511,7 @@ fn parity_swap_four( if (gt) { // 3 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 3); + inc_n_data(inc_n_context, cmp_data, 3); } copy(tmp_ptr, arr_ptr); copy(arr_ptr, arr_ptr + element_width); @@ -2499,12 +2533,13 @@ fn parity_swap_five( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { // 4 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 4); + inc_n_data(inc_n_context, cmp_data, 4); } var arr_ptr = array; swap_branchless(arr_ptr, tmp_ptr, cmp, cmp_data, element_width, copy, indirect); @@ -2519,7 +2554,7 @@ fn parity_swap_five( if (more_work != 0) { // 6 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 6); + inc_n_data(inc_n_context, cmp_data, 6); } swap_branchless(arr_ptr, tmp_ptr, cmp, cmp_data, element_width, copy, indirect); arr_ptr += 2 * element_width; @@ -2544,12 +2579,13 @@ fn parity_swap_six( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { // 7 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 5); + inc_n_data(inc_n_context, cmp_data, 5); } var arr_ptr = array; swap_branchless(arr_ptr, tmp_ptr, cmp, cmp_data, element_width, copy, indirect); @@ -2566,7 +2602,7 @@ fn parity_swap_six( if (lte) { // 2 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 2); + inc_n_data(inc_n_context, cmp_data, 2); } swap_branchless(arr_ptr, tmp_ptr, cmp, cmp_data, element_width, copy, indirect); arr_ptr += 4 * element_width; @@ -2577,7 +2613,7 @@ fn parity_swap_six( // 8 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 8); + inc_n_data(inc_n_context, cmp_data, 8); } { const gt = compare(cmp, cmp_data, arr_ptr, arr_ptr + element_width, indirect) == GT; @@ -2625,12 +2661,13 @@ fn parity_swap_seven( element_width: usize, copy: CopyFn, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) void { // 6 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 6); + inc_n_data(inc_n_context, cmp_data, 6); } var arr_ptr = array; swap_branchless(arr_ptr, tmp_ptr, cmp, cmp_data, element_width, copy, indirect); @@ -2651,7 +2688,7 @@ fn parity_swap_seven( // 11 guaranteed compares. if (data_is_owned) { - inc_n_data(cmp_data, 11); + inc_n_data(inc_n_context, cmp_data, 11); } swap_branchless(arr_ptr, tmp_ptr, cmp, cmp_data, element_width, copy, indirect); arr_ptr = array; @@ -2701,7 +2738,7 @@ fn parity_swap_seven( copy(arr_ptr, from); } -// ================ Primitives ================================================ +// Primitives // Below are sorting primitives that attempt to be branchless. // They all also are always inline for performance. // The are the smallest fundamental unit. @@ -2860,12 +2897,12 @@ inline fn compare( comptime indirect: bool, ) Ordering { if (indirect) { - const lhs = @as(*[*]u8, @ptrCast(@alignCast(lhs_opaque))).*; - const rhs = @as(*[*]u8, @ptrCast(@alignCast(rhs_opaque))).*; - return @as(Ordering, @enumFromInt(cmp(cmp_data, lhs, rhs))); + const lhs_ptr: *[*]u8 = utils.alignedPtrCast(*[*]u8, @as([*]u8, @ptrCast(lhs_opaque)), @src()); + const rhs_ptr: *[*]u8 = utils.alignedPtrCast(*[*]u8, @as([*]u8, @ptrCast(rhs_opaque)), @src()); + return @as(Ordering, @enumFromInt(cmp(cmp_data, lhs_ptr.*, rhs_ptr.*))); } else { - const lhs = @as([*]u8, @ptrCast(@alignCast(lhs_opaque))); - const rhs = @as([*]u8, @ptrCast(@alignCast(rhs_opaque))); + const lhs: [*]u8 = @ptrCast(lhs_opaque); + const rhs: [*]u8 = @ptrCast(rhs_opaque); return @as(Ordering, @enumFromInt(cmp(cmp_data, lhs, rhs))); } } @@ -2880,27 +2917,30 @@ inline fn compare_inc( lhs: [*]u8, rhs: [*]u8, comptime data_is_owned: bool, + inc_n_context: ?*anyopaque, inc_n_data: IncN, comptime indirect: bool, ) Ordering { if (data_is_owned) { - inc_n_data(cmp_data, 1); + inc_n_data(inc_n_context, cmp_data, 1); } return compare(cmp, cmp_data, lhs, rhs, indirect); } /// Copies the value pointed to by `src_ptr` into the location pointed to by `dst_ptr`. /// Both pointers must be valid and properly aligned. -pub fn pointer_copy(dst_ptr: Opaque, src_ptr: Opaque) callconv(.C) void { - @as(*usize, @alignCast(@ptrCast(dst_ptr))).* = @as(*usize, @alignCast(@ptrCast(src_ptr))).*; +pub fn pointer_copy(dst_ptr: Opaque, src_ptr: Opaque) callconv(.c) void { + const dst: *usize = utils.alignedPtrCast(*usize, dst_ptr.?, @src()); + const src: *const usize = utils.alignedPtrCast(*const usize, src_ptr.?, @src()); + dst.* = src.*; } -fn test_i64_compare(_: Opaque, a_ptr: Opaque, b_ptr: Opaque) callconv(.C) u8 { - const a = @as(*i64, @alignCast(@ptrCast(a_ptr))).*; - const b = @as(*i64, @alignCast(@ptrCast(b_ptr))).*; +fn test_i64_compare(_: Opaque, a_ptr: Opaque, b_ptr: Opaque) callconv(.c) u8 { + const a: *const i64 = utils.alignedPtrCast(*const i64, a_ptr.?, @src()); + const b: *const i64 = utils.alignedPtrCast(*const i64, b_ptr.?, @src()); - const gt = @as(u8, @intFromBool(a > b)); - const lt = @as(u8, @intFromBool(a < b)); + const gt = @as(u8, @intFromBool(a.* > b.*)); + const lt = @as(u8, @intFromBool(a.* < b.*)); // Eq = 0 // GT = 1 @@ -2908,26 +2948,30 @@ fn test_i64_compare(_: Opaque, a_ptr: Opaque, b_ptr: Opaque) callconv(.C) u8 { return lt + lt + gt; } -fn test_i64_compare_refcounted(count_ptr: Opaque, a_ptr: Opaque, b_ptr: Opaque) callconv(.C) u8 { - const a = @as(*i64, @alignCast(@ptrCast(a_ptr))).*; - const b = @as(*i64, @alignCast(@ptrCast(b_ptr))).*; +fn test_i64_compare_refcounted(count_ptr: Opaque, a_ptr: Opaque, b_ptr: Opaque) callconv(.c) u8 { + const a: *const i64 = utils.alignedPtrCast(*const i64, a_ptr.?, @src()); + const b: *const i64 = utils.alignedPtrCast(*const i64, b_ptr.?, @src()); - const gt = @as(u8, @intFromBool(a > b)); - const lt = @as(u8, @intFromBool(a < b)); + const gt = @as(u8, @intFromBool(a.* > b.*)); + const lt = @as(u8, @intFromBool(a.* < b.*)); - @as(*isize, @ptrCast(@alignCast(count_ptr))).* -= 1; + const count: *isize = utils.alignedPtrCast(*isize, count_ptr.?, @src()); + count.* -= 1; // Eq = 0 // GT = 1 // LT = 2 return lt + lt + gt; } -fn test_i64_copy(dst_ptr: Opaque, src_ptr: Opaque) callconv(.C) void { - @as(*i64, @alignCast(@ptrCast(dst_ptr))).* = @as(*i64, @alignCast(@ptrCast(src_ptr))).*; +fn test_i64_copy(dst_ptr: Opaque, src_ptr: Opaque) callconv(.c) void { + const dst: *i64 = utils.alignedPtrCast(*i64, dst_ptr.?, @src()); + const src: *const i64 = utils.alignedPtrCast(*const i64, src_ptr.?, @src()); + dst.* = src.*; } -fn test_inc_n_data(count_ptr: Opaque, n: usize) callconv(.C) void { - @as(*isize, @ptrCast(@alignCast(count_ptr))).* += @intCast(n); +fn test_inc_n_data(_: ?*anyopaque, count_ptr: Opaque, n: usize) callconv(.c) void { + const count: *isize = utils.alignedPtrCast(*isize, count_ptr.?, @src()); + count.* += @intCast(n); } test "flux_default_partition" { @@ -2955,7 +2999,7 @@ test "flux_default_partition" { 18, 20, 22, 24, 26, 28, 30, 32, }; pivot = 16; - var arr_len = flux_default_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + var arr_len = flux_default_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr_len, 16); try std.testing.expectEqualSlices(i64, arr[0..16], expected[0..16]); @@ -2976,7 +3020,7 @@ test "flux_default_partition" { 26, 28, 30, 32, }; pivot = 24; - arr_len = flux_default_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + arr_len = flux_default_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr_len, 24); try std.testing.expectEqualSlices(i64, arr[0..24], expected[0..24]); @@ -2993,7 +3037,7 @@ test "flux_default_partition" { 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, }; pivot = 32; - arr_len = flux_default_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + arr_len = flux_default_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr_len, 32); try std.testing.expectEqualSlices(i64, arr[0..32], expected[0..32]); @@ -3008,7 +3052,7 @@ test "flux_default_partition" { expected[i] = @intCast(i + 1); } pivot = 16; - arr_len = flux_default_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + arr_len = flux_default_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr_len, 0); try std.testing.expectEqualSlices(i64, arr[0..32], expected[0..32]); @@ -3032,7 +3076,7 @@ test "flux_reverse_partition" { 2, 4, 6, 8, 10, 12, 14, 16, 17, 17, 17, 17, 17, 17, 17, 17, }; pivot = 17; - flux_reverse_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + flux_reverse_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); @@ -3041,7 +3085,7 @@ test "flux_reverse_partition" { 17, 2, 17, 4, 17, 6, 17, 8, 17, 10, 17, 12, 17, 14, 17, 16, }; pivot = 17; - flux_reverse_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + flux_reverse_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); @@ -3050,7 +3094,7 @@ test "flux_reverse_partition" { 17, 16, 17, 14, 17, 12, 17, 10, 17, 8, 17, 6, 17, 4, 17, 2, }; pivot = 17; - flux_reverse_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + flux_reverse_partition(arr_ptr, swap_ptr, arr_ptr, @ptrCast(&pivot), 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); } @@ -3070,7 +3114,7 @@ test "median_of_cube_root" { 1, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, }; - median_of_cube_root(arr_ptr, swap_ptr, arr_ptr, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, @ptrCast(&generic), @ptrCast(&out), false); + median_of_cube_root(arr_ptr, swap_ptr, arr_ptr, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, @ptrCast(&generic), @ptrCast(&out), false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(out, 17); try std.testing.expectEqual(generic, false); @@ -3078,7 +3122,7 @@ test "median_of_cube_root" { for (0..32) |i| { arr[i] = 7; } - median_of_cube_root(arr_ptr, swap_ptr, arr_ptr, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, @ptrCast(&generic), @ptrCast(&out), false); + median_of_cube_root(arr_ptr, swap_ptr, arr_ptr, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, @ptrCast(&generic), @ptrCast(&out), false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(out, 7); try std.testing.expectEqual(generic, true); @@ -3086,7 +3130,7 @@ test "median_of_cube_root" { for (0..32) |i| { arr[i] = 7 + @as(i64, @intCast(i % 2)); } - median_of_cube_root(arr_ptr, swap_ptr, arr_ptr, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, @ptrCast(&generic), @ptrCast(&out), false); + median_of_cube_root(arr_ptr, swap_ptr, arr_ptr, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, @ptrCast(&generic), @ptrCast(&out), false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(out, 8); try std.testing.expectEqual(generic, false); @@ -3102,21 +3146,21 @@ test "median_of_nine" { const arr_ptr = @as([*]u8, @ptrCast(&arr[0])); arr = [9]i64{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }; - median_of_nine(arr_ptr, 10, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, @ptrCast(&out), false); + median_of_nine(arr_ptr, 10, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, @ptrCast(&out), false); try std.testing.expectEqual(test_count, 0); // Note: median is not guaranteed to be exact. in this case: // [2, 3], [6, 7] -> [3, 6] -> [3, 6, 9] -> 6 try std.testing.expectEqual(out, 6); arr = [9]i64{ 1, 3, 5, 7, 9, 2, 4, 6, 8 }; - median_of_nine(arr_ptr, 10, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, @ptrCast(&out), false); + median_of_nine(arr_ptr, 10, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, @ptrCast(&out), false); try std.testing.expectEqual(test_count, 0); // Note: median is not guaranteed to be exact. in this case: // [3, 5], [4, 6] -> [4, 5] -> [4, 5, 8] -> 5 try std.testing.expectEqual(out, 5); arr = [9]i64{ 2, 3, 9, 4, 5, 7, 8, 6, 1 }; - median_of_nine(arr_ptr, 10, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, @ptrCast(&out), false); + median_of_nine(arr_ptr, 10, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, @ptrCast(&out), false); try std.testing.expectEqual(test_count, 0); // Note: median is not guaranteed to be exact. in this case: // [3, 4], [5, 6] -> [4, 5] -> [1, 4, 5] -> 4 @@ -3131,17 +3175,17 @@ test "trim_four" { const arr_ptr = @as([*]u8, @ptrCast(&arr[0])); arr = [4]i64{ 1, 2, 3, 4 }; - trim_four(arr_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + trim_four(arr_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [4]i64{ 1, 2, 3, 4 }); arr = [4]i64{ 2, 3, 1, 4 }; - trim_four(arr_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + trim_four(arr_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [4]i64{ 2, 3, 2, 4 }); arr = [4]i64{ 4, 3, 2, 1 }; - trim_four(arr_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + trim_four(arr_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [4]i64{ 3, 2, 3, 2 }); } @@ -3155,12 +3199,12 @@ test "binary_median" { const arr_ptr = @as([*]u8, @ptrCast(&arr[0])); arr = [10]i64{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; - binary_median(arr_ptr, arr_ptr + 5 * @sizeOf(i64), 5, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, @ptrCast(&out), false); + binary_median(arr_ptr, arr_ptr + 5 * @sizeOf(i64), 5, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, @ptrCast(&out), false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(out, 6); arr = [10]i64{ 1, 3, 5, 7, 9, 2, 4, 6, 8, 10 }; - binary_median(arr_ptr, arr_ptr + 5 * @sizeOf(i64), 5, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, @ptrCast(&out), false); + binary_median(arr_ptr, arr_ptr + 5 * @sizeOf(i64), 5, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, @ptrCast(&out), false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(out, 5); } @@ -3169,17 +3213,17 @@ test "binary_median" { const arr_ptr = @as([*]u8, @ptrCast(&arr[0])); arr = [16]i64{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 }; - binary_median(arr_ptr, arr_ptr + 8 * @sizeOf(i64), 8, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, @ptrCast(&out), false); + binary_median(arr_ptr, arr_ptr + 8 * @sizeOf(i64), 8, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, @ptrCast(&out), false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(out, 9); arr = [16]i64{ 1, 3, 5, 7, 9, 11, 13, 15, 2, 4, 6, 8, 10, 12, 14, 16 }; - binary_median(arr_ptr, arr_ptr + 8 * @sizeOf(i64), 8, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, @ptrCast(&out), false); + binary_median(arr_ptr, arr_ptr + 8 * @sizeOf(i64), 8, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, @ptrCast(&out), false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(out, 9); arr = [16]i64{ 9, 10, 11, 12, 13, 14, 15, 16, 1, 2, 3, 4, 5, 6, 7, 8 }; - binary_median(arr_ptr, arr_ptr + 8 * @sizeOf(i64), 8, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, @ptrCast(&out), false); + binary_median(arr_ptr, arr_ptr + 8 * @sizeOf(i64), 8, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, @ptrCast(&out), false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(out, 9); } @@ -3195,23 +3239,23 @@ test "rotate_merge" { const swap_ptr = @as([*]u8, @ptrCast(&swap[0])); arr = [10]i64{ 7, 8, 5, 6, 3, 4, 1, 2, 9, 10 }; - rotate_merge(arr_ptr, 10, swap_ptr, 10, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + rotate_merge(arr_ptr, 10, swap_ptr, 10, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); arr = [10]i64{ 7, 8, 5, 6, 3, 4, 1, 9, 2, 10 }; - rotate_merge(arr_ptr, 9, swap_ptr, 9, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + rotate_merge(arr_ptr, 9, swap_ptr, 9, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); arr = [10]i64{ 3, 4, 6, 9, 1, 2, 5, 10, 7, 8 }; - rotate_merge(arr_ptr, 10, swap_ptr, 10, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + rotate_merge(arr_ptr, 10, swap_ptr, 10, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); // Limited swap, can't finish merge arr = [10]i64{ 7, 8, 5, 6, 3, 4, 1, 9, 2, 10 }; - rotate_merge(arr_ptr, 10, swap_ptr, 4, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + rotate_merge(arr_ptr, 10, swap_ptr, 4, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); } @@ -3225,27 +3269,27 @@ test "monobound_binary_first" { const value_ptr = @as([*]u8, @ptrCast(&value)); value = 7; - var res = monobound_binary_first(arr_ptr, 25, value_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), true, &test_inc_n_data, false); + var res = monobound_binary_first(arr_ptr, 25, value_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(res, 3); value = 39; - res = monobound_binary_first(arr_ptr, 25, value_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), true, &test_inc_n_data, false); + res = monobound_binary_first(arr_ptr, 25, value_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(res, 19); value = 40; - res = monobound_binary_first(arr_ptr, 25, value_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), true, &test_inc_n_data, false); + res = monobound_binary_first(arr_ptr, 25, value_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(res, 20); value = -10; - res = monobound_binary_first(arr_ptr, 25, value_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), true, &test_inc_n_data, false); + res = monobound_binary_first(arr_ptr, 25, value_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(res, 0); value = 10000; - res = monobound_binary_first(arr_ptr, 25, value_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), true, &test_inc_n_data, false); + res = monobound_binary_first(arr_ptr, 25, value_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(res, 25); } @@ -3310,17 +3354,17 @@ test "tail_merge" { const swap_ptr = @as([*]u8, @ptrCast(&swap[0])); arr = [10]i64{ 7, 8, 5, 6, 3, 4, 1, 2, 9, 10 }; - tail_merge(arr_ptr, 10, swap_ptr, 10, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tail_merge(arr_ptr, 10, swap_ptr, 10, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); arr = [10]i64{ 7, 8, 5, 6, 3, 4, 1, 2, 9, 10 }; - tail_merge(arr_ptr, 9, swap_ptr, 9, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tail_merge(arr_ptr, 9, swap_ptr, 9, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); arr = [10]i64{ 3, 4, 6, 9, 1, 2, 5, 10, 7, 8 }; - tail_merge(arr_ptr, 10, swap_ptr, 10, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tail_merge(arr_ptr, 10, swap_ptr, 10, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); } @@ -3336,22 +3380,22 @@ test "partial_backwards_merge" { const swap_ptr = @as([*]u8, @ptrCast(&swap[0])); arr = [10]i64{ 3, 4, 5, 6, 7, 8, 1, 2, 9, 10 }; - partial_backwards_merge(arr_ptr, 10, swap_ptr, 10, 6, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + partial_backwards_merge(arr_ptr, 10, swap_ptr, 10, 6, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); arr = [10]i64{ 2, 4, 6, 8, 9, 10, 1, 3, 5, 7 }; - partial_backwards_merge(arr_ptr, 10, swap_ptr, 10, 6, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + partial_backwards_merge(arr_ptr, 10, swap_ptr, 10, 6, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); arr = [10]i64{ 1, 2, 3, 4, 5, 6, 8, 9, 10, 7 }; - partial_backwards_merge(arr_ptr, 10, swap_ptr, 10, 9, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + partial_backwards_merge(arr_ptr, 10, swap_ptr, 10, 9, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); arr = [10]i64{ 1, 2, 4, 5, 6, 8, 9, 3, 7, 10 }; - partial_backwards_merge(arr_ptr, 10, swap_ptr, 9, 7, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + partial_backwards_merge(arr_ptr, 10, swap_ptr, 9, 7, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); } @@ -3380,7 +3424,7 @@ test "partial_backwards_merge" { for (0..16) |i| { arr[i + 48] = @intCast(i + 33); } - partial_backwards_merge(arr_ptr, 64, swap_ptr, 64, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + partial_backwards_merge(arr_ptr, 64, swap_ptr, 64, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); @@ -3400,7 +3444,7 @@ test "partial_backwards_merge" { arr[16] = 33; arr[63] = 49; - partial_backwards_merge(arr_ptr, 64, swap_ptr, 64, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + partial_backwards_merge(arr_ptr, 64, swap_ptr, 64, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); } @@ -3416,22 +3460,22 @@ test "partial_forward_merge" { const swap_ptr = @as([*]u8, @ptrCast(&swap[0])); arr = [10]i64{ 3, 4, 5, 6, 7, 8, 1, 2, 9, 10 }; - partial_forward_merge(arr_ptr, 10, swap_ptr, 10, 6, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + partial_forward_merge(arr_ptr, 10, swap_ptr, 10, 6, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); arr = [10]i64{ 2, 4, 6, 8, 9, 10, 1, 3, 5, 7 }; - partial_forward_merge(arr_ptr, 10, swap_ptr, 10, 6, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + partial_forward_merge(arr_ptr, 10, swap_ptr, 10, 6, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); arr = [10]i64{ 1, 2, 3, 4, 5, 6, 8, 9, 10, 7 }; - partial_forward_merge(arr_ptr, 10, swap_ptr, 10, 9, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + partial_forward_merge(arr_ptr, 10, swap_ptr, 10, 9, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); arr = [10]i64{ 1, 2, 4, 5, 6, 8, 9, 3, 7, 10 }; - partial_forward_merge(arr_ptr, 10, swap_ptr, 9, 7, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + partial_forward_merge(arr_ptr, 10, swap_ptr, 9, 7, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); } @@ -3447,32 +3491,32 @@ test "quad_merge" { var size: usize = undefined; arr = [10]i64{ 7, 8, 5, 6, 3, 4, 1, 2, 9, 10 }; - size = quad_merge(arr_ptr, 10, swap_ptr, 10, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + size = quad_merge(arr_ptr, 10, swap_ptr, 10, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); try std.testing.expectEqual(size, 16); arr = [10]i64{ 7, 8, 5, 6, 3, 4, 1, 9, 2, 10 }; - size = quad_merge(arr_ptr, 9, swap_ptr, 9, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + size = quad_merge(arr_ptr, 9, swap_ptr, 9, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); try std.testing.expectEqual(size, 16); arr = [10]i64{ 3, 4, 6, 9, 1, 2, 5, 10, 7, 8 }; - size = quad_merge(arr_ptr, 10, swap_ptr, 10, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + size = quad_merge(arr_ptr, 10, swap_ptr, 10, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); try std.testing.expectEqual(size, 8); // Limited swap, can't finish merge arr = [10]i64{ 7, 8, 5, 6, 3, 4, 1, 9, 2, 10 }; - size = quad_merge(arr_ptr, 10, swap_ptr, 4, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + size = quad_merge(arr_ptr, 10, swap_ptr, 4, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [10]i64{ 1, 3, 4, 5, 6, 7, 8, 9, 2, 10 }); try std.testing.expectEqual(size, 4); arr = [10]i64{ 7, 8, 5, 6, 3, 4, 1, 9, 2, 10 }; - size = quad_merge(arr_ptr, 10, swap_ptr, 3, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + size = quad_merge(arr_ptr, 10, swap_ptr, 3, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [10]i64{ 5, 6, 7, 8, 1, 3, 4, 9, 2, 10 }); try std.testing.expectEqual(size, 4); @@ -3489,31 +3533,31 @@ test "quad_merge_block" { // case 0 - totally unsorted arr = [8]i64{ 7, 8, 5, 6, 3, 4, 1, 2 }; - quad_merge_block(arr_ptr, swap_ptr, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + quad_merge_block(arr_ptr, swap_ptr, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); // case 1 - first half sorted arr = [8]i64{ 5, 6, 7, 8, 3, 4, 1, 2 }; - quad_merge_block(arr_ptr, swap_ptr, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + quad_merge_block(arr_ptr, swap_ptr, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); // case 2 - second half sorted arr = [8]i64{ 7, 8, 5, 6, 1, 2, 3, 4 }; - quad_merge_block(arr_ptr, swap_ptr, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + quad_merge_block(arr_ptr, swap_ptr, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); // case 3 both haves sorted arr = [8]i64{ 1, 3, 5, 7, 2, 4, 6, 8 }; - quad_merge_block(arr_ptr, swap_ptr, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + quad_merge_block(arr_ptr, swap_ptr, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); // case 3 - lucky, sorted arr = [8]i64{ 1, 2, 3, 4, 5, 6, 7, 8 }; - quad_merge_block(arr_ptr, swap_ptr, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + quad_merge_block(arr_ptr, swap_ptr, 2, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); // try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); } @@ -3537,7 +3581,7 @@ test "cross_merge" { for (0..32) |i| { src[i + 32] = @intCast(i + 1); } - cross_merge(dest_ptr, src_ptr, 32, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + cross_merge(dest_ptr, src_ptr, 32, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(dest, expected); @@ -3546,7 +3590,7 @@ test "cross_merge" { src[i * 2] = @intCast(i * 2 + 1); src[i * 2 + 1] = @intCast(i * 2 + 2); } - cross_merge(dest_ptr, src_ptr, 32, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + cross_merge(dest_ptr, src_ptr, 32, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(dest, expected); @@ -3557,7 +3601,7 @@ test "cross_merge" { for (0..44) |i| { src[i + 20] = @intCast(i + 1); } - cross_merge(dest_ptr, src_ptr, 20, 44, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + cross_merge(dest_ptr, src_ptr, 20, 44, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(dest, expected); @@ -3574,7 +3618,7 @@ test "cross_merge" { for (0..16) |i| { src[i + 48] = @intCast(i + 33); } - cross_merge(dest_ptr, src_ptr, 32, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + cross_merge(dest_ptr, src_ptr, 32, 32, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(dest, expected); } @@ -3604,7 +3648,7 @@ test "quad_swap" { 72, 58, 57, }; - var result = quad_swap(arr_ptr, 75, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + var result = quad_swap(arr_ptr, 75, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(result, .unfinished); try std.testing.expectEqual(arr, [75]i64{ @@ -3629,7 +3673,7 @@ test "quad_swap" { expected[i] = @intCast(i + 1); arr[i] = @intCast(75 - i); } - result = quad_swap(arr_ptr, 75, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + result = quad_swap(arr_ptr, 75, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(result, .sorted); try std.testing.expectEqual(arr, expected); @@ -3691,7 +3735,7 @@ test "tail_swap" { var rng = std.Random.DefaultPrng.init(seed); rng.random().shuffle(i64, arr[0..]); - tail_swap(arr_ptr, 31, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tail_swap(arr_ptr, 31, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, expected); } @@ -3708,13 +3752,13 @@ test "parity_merge" { arr = [8]i64{ 1, 3, 5, 7, 2, 4, 6, 8 }; dest = [8]i64{ 0, 0, 0, 0, 0, 0, 0, 0 }; - parity_merge(dest_ptr, arr_ptr, 4, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + parity_merge(dest_ptr, arr_ptr, 4, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(dest, [8]i64{ 1, 2, 3, 4, 5, 6, 7, 8 }); arr = [8]i64{ 5, 6, 7, 8, 1, 2, 3, 4 }; dest = [8]i64{ 0, 0, 0, 0, 0, 0, 0, 0 }; - parity_merge(dest_ptr, arr_ptr, 4, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + parity_merge(dest_ptr, arr_ptr, 4, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(dest, [8]i64{ 1, 2, 3, 4, 5, 6, 7, 8 }); } @@ -3727,25 +3771,25 @@ test "parity_merge" { arr = [9]i64{ 1, 3, 5, 8, 2, 4, 6, 7, 9 }; dest = [9]i64{ 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - parity_merge(dest_ptr, arr_ptr, 4, 5, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + parity_merge(dest_ptr, arr_ptr, 4, 5, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(dest, [9]i64{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }); arr = [9]i64{ 6, 7, 8, 9, 1, 2, 3, 4, 5 }; dest = [9]i64{ 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - parity_merge(dest_ptr, arr_ptr, 4, 5, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + parity_merge(dest_ptr, arr_ptr, 4, 5, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(dest, [9]i64{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }); arr = [9]i64{ 1, 3, 5, 7, 8, 2, 4, 6, 9 }; dest = [9]i64{ 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - parity_merge(dest_ptr, arr_ptr, 5, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + parity_merge(dest_ptr, arr_ptr, 5, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(dest, [9]i64{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }); arr = [9]i64{ 5, 6, 7, 8, 9, 1, 2, 3, 4 }; dest = [9]i64{ 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - parity_merge(dest_ptr, arr_ptr, 5, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + parity_merge(dest_ptr, arr_ptr, 5, 4, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(dest, [9]i64{ 1, 2, 3, 4, 5, 6, 7, 8, 9 }); } @@ -3761,12 +3805,12 @@ test "tiny_sort" { const arr_ptr = @as([*]u8, @ptrCast(&arr[0])); arr = [7]i64{ 3, 1, 2, 5, 4, 7, 6 }; - tiny_sort(arr_ptr, 7, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tiny_sort(arr_ptr, 7, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [7]i64{ 1, 2, 3, 4, 5, 6, 7 }); arr = [7]i64{ 7, 6, 5, 4, 3, 2, 1 }; - tiny_sort(arr_ptr, 7, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tiny_sort(arr_ptr, 7, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [7]i64{ 1, 2, 3, 4, 5, 6, 7 }); } @@ -3775,12 +3819,12 @@ test "tiny_sort" { const arr_ptr = @as([*]u8, @ptrCast(&arr[0])); arr = [6]i64{ 3, 1, 2, 6, 4, 5 }; - tiny_sort(arr_ptr, 6, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tiny_sort(arr_ptr, 6, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [6]i64{ 1, 2, 3, 4, 5, 6 }); arr = [6]i64{ 6, 5, 4, 3, 2, 1 }; - tiny_sort(arr_ptr, 6, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tiny_sort(arr_ptr, 6, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [6]i64{ 1, 2, 3, 4, 5, 6 }); } @@ -3789,12 +3833,12 @@ test "tiny_sort" { const arr_ptr = @as([*]u8, @ptrCast(&arr[0])); arr = [5]i64{ 2, 1, 4, 3, 5 }; - tiny_sort(arr_ptr, 5, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tiny_sort(arr_ptr, 5, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [5]i64{ 1, 2, 3, 4, 5 }); arr = [5]i64{ 5, 4, 3, 2, 1 }; - tiny_sort(arr_ptr, 5, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tiny_sort(arr_ptr, 5, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [5]i64{ 1, 2, 3, 4, 5 }); } @@ -3803,26 +3847,26 @@ test "tiny_sort" { const arr_ptr = @as([*]u8, @ptrCast(&arr[0])); arr = [4]i64{ 4, 2, 1, 3 }; - tiny_sort(arr_ptr, 4, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tiny_sort(arr_ptr, 4, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [4]i64{ 1, 2, 3, 4 }); arr = [4]i64{ 2, 1, 4, 3 }; - tiny_sort(arr_ptr, 4, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tiny_sort(arr_ptr, 4, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [4]i64{ 1, 2, 3, 4 }); } { var arr = [3]i64{ 2, 3, 1 }; const arr_ptr = @as([*]u8, @ptrCast(&arr[0])); - tiny_sort(arr_ptr, 3, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tiny_sort(arr_ptr, 3, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [3]i64{ 1, 2, 3 }); } { var arr = [2]i64{ 2, 1 }; const arr_ptr = @as([*]u8, @ptrCast(&arr[0])); - tiny_sort(arr_ptr, 2, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, &test_inc_n_data, false); + tiny_sort(arr_ptr, 2, swap_ptr, &test_i64_compare_refcounted, @ptrCast(&test_count), @sizeOf(i64), &test_i64_copy, true, null, &test_inc_n_data, false); try std.testing.expectEqual(test_count, 0); try std.testing.expectEqual(arr, [2]i64{ 1, 2 }); } diff --git a/src/builtins/str.zig b/src/builtins/str.zig index 43b3cfa8e3..35c5b5faf3 100644 --- a/src/builtins/str.zig +++ b/src/builtins/str.zig @@ -4,7 +4,20 @@ //! operations for string manipulation, Unicode handling, formatting, and //! memory management. It defines the RocStr structure and associated functions //! that are called from compiled Roc code to handle string operations efficiently. +//! +//! ## Ownership Semantics +//! +//! See `OWNERSHIP.md` for the canonical terminology. Functions in this module +//! follow these patterns: +//! +//! - **Borrow**: Function reads argument, caller retains ownership +//! - **Consume**: Function takes ownership, caller loses access +//! - **Copy-on-Write**: Consumes arg; if unique, mutates in place; if shared, allocates new +//! - **Seamless Slice**: Result shares data with arg via incref'd slice +//! +//! Each function documents its ownership semantics in its doc comment. const std = @import("std"); +const builtin = @import("builtin"); const RocList = @import("list.zig").RocList; const RocOps = @import("host_abi.zig").RocOps; @@ -18,6 +31,23 @@ const unicode = std.unicode; const testing = std.testing; const rcNone = @import("utils.zig").rcNone; +/// Decref function for RocStr elements in a list. +/// Used when decref-ing a List Str - each string element needs to be decreffed. +/// The context parameter is expected to be a *RocOps. +fn strDecref(context: ?*anyopaque, element: ?[*]u8) callconv(.c) void { + if (element) |elem_ptr| { + const str_ptr: *RocStr = utils.alignedPtrCast(*RocStr, elem_ptr, @src()); + if (context) |ctx| { + const roc_ops: *RocOps = utils.alignedPtrCast(*RocOps, @as([*]u8, @ptrCast(ctx)), @src()); + str_ptr.decref(roc_ops); + } else { + // Context should never be null - this is a programming error + // We cannot call roc_ops.crash() because we don't have roc_ops + unreachable; + } + } +} + const InPlace = enum(u8) { InPlace, Clone, @@ -78,7 +108,7 @@ pub const RocStr = extern struct { // This requires that the list is non-null. // It also requires that start and count define a slice that does not go outside the bounds of the list. - pub fn fromSubListUnsafe(list: RocList, start: usize, count: usize, update_mode: UpdateMode) RocStr { + pub fn fromSubListUnsafe(list: RocList, start: usize, count: usize, update_mode: UpdateMode, roc_ops: *RocOps) RocStr { const start_byte = @as([*]u8, @ptrCast(list.bytes)) + start; if (list.isSeamlessSlice()) { return RocStr{ @@ -86,7 +116,7 @@ pub const RocStr = extern struct { .length = count | SEAMLESS_SLICE_BIT, .capacity_or_alloc_ptr = list.capacity_or_alloc_ptr & (~SEAMLESS_SLICE_BIT), }; - } else if (start == 0 and (update_mode == .InPlace or list.isUnique())) { + } else if (start == 0 and (update_mode == .InPlace or list.isUnique(roc_ops))) { // Rare case, we can take over the original list. return RocStr{ .bytes = start_byte, @@ -114,6 +144,23 @@ pub const RocStr = extern struct { return RocStr.init(slice.ptr, slice.len, roc_ops); } + /// Create a small string from a slice. The slice must fit in a small string + /// (length < SMALL_STRING_SIZE). This does not require roc_ops since small + /// strings are stored inline and don't need heap allocation. + /// Asserts in debug mode if the slice is too large. + pub fn fromSliceSmall(slice: []const u8) RocStr { + std.debug.assert(slice.len < SMALL_STRING_SIZE); + var result = RocStr.empty(); + @memcpy(result.asU8ptrMut()[0..slice.len], slice); + result.asU8ptrMut()[@sizeOf(RocStr) - 1] = @as(u8, @intCast(slice.len)) | 0b1000_0000; + return result; + } + + /// Returns true if the given length would fit in a small string (stored inline). + pub fn fitsInSmallStr(length: usize) bool { + return length < SMALL_STRING_SIZE; + } + fn allocateBig( length: usize, capacity: usize, @@ -191,15 +238,23 @@ pub const RocStr = extern struct { const slice_alloc_ptr = self.capacity_or_alloc_ptr << 1; const slice_mask = self.seamlessSliceMask(); const alloc_ptr = (str_alloc_ptr & ~slice_mask) | (slice_alloc_ptr & slice_mask); + + // Verify the computed allocation pointer is properly aligned + if (comptime builtin.mode == .Debug) { + if (alloc_ptr != 0 and alloc_ptr % @alignOf(usize) != 0) { + // This indicates memory corruption - the allocation pointer should always be aligned + unreachable; + } + } + return @as(?[*]u8, @ptrFromInt(alloc_ptr)); } - pub fn incref(self: RocStr, n: usize) void { + pub fn incref(self: RocStr, n: usize, roc_ops: *RocOps) void { if (!self.isSmallStr()) { - const alloc_ptr = self.getAllocationPtr(); - if (alloc_ptr != null) { - const isizes: [*]isize = @as([*]isize, @ptrCast(@alignCast(alloc_ptr))); - @import("utils.zig").increfRcPtrC(@as(*isize, @ptrCast(isizes - 1)), @as(isize, @intCast(n))); + if (self.getAllocationPtr()) |alloc_ptr| { + const isizes: [*]isize = utils.alignedPtrCast([*]isize, alloc_ptr, @src()); + utils.increfRcPtrC(@as(*isize, @ptrCast(isizes - 1)), @as(isize, @intCast(n)), roc_ops); } } } @@ -213,7 +268,7 @@ pub const RocStr = extern struct { } } - pub fn eq(self: RocStr, other: RocStr) bool { + pub fn eql(self: RocStr, other: RocStr) bool { // If they are byte-for-byte equal, they're definitely equal! if (self.bytes == other.bytes and self.length == other.length) { return true; @@ -244,6 +299,25 @@ pub const RocStr = extern struct { return true; } + /// Compare this RocStr with a byte slice for equality. + pub fn eqlSlice(self: RocStr, slice: []const u8) bool { + const self_len = self.len(); + + if (self_len != slice.len) { + return false; + } + + const self_bytes = self.asU8ptr(); + var b: usize = 0; + while (b < self_len) : (b += 1) { + if (self_bytes[b] != slice[b]) { + return false; + } + } + + return true; + } + pub fn clone( str: RocStr, roc_ops: *RocOps, @@ -252,12 +326,15 @@ pub const RocStr = extern struct { // just return the bytes return str; } else { - const new_str = RocStr.allocateBig(str.length, str.length, roc_ops); + // Use len() instead of .length to handle seamless slices correctly. + // For seamless slices, .length has the SEAMLESS_SLICE_BIT set. + const length = str.len(); + const new_str = RocStr.allocateBig(length, length, roc_ops); - var old_bytes: [*]u8 = @as([*]u8, @ptrCast(str.bytes)); - var new_bytes: [*]u8 = @as([*]u8, @ptrCast(new_str.bytes)); + const old_bytes: [*]u8 = @as([*]u8, @ptrCast(str.bytes)); + const new_bytes: [*]u8 = @as([*]u8, @ptrCast(new_str.bytes)); - @memcpy(new_bytes[0..str.length], old_bytes[0..str.length]); + @memcpy(new_bytes[0..length], old_bytes[0..length]); return new_str; } @@ -289,6 +366,7 @@ pub const RocStr = extern struct { new_capacity, element_width, false, + roc_ops, ); return RocStr{ .bytes = new_source, .length = new_length, .capacity_or_alloc_ptr = new_capacity }; @@ -352,6 +430,10 @@ pub const RocStr = extern struct { return slice.*; } + pub fn is_empty(self: RocStr) bool { + return self.len() == 0; + } + pub fn len(self: RocStr) usize { if (self.isSmallStr()) { return self.asArray()[@sizeOf(RocStr) - 1] ^ 0b1000_0000; @@ -418,8 +500,13 @@ pub const RocStr = extern struct { else self.bytes; - const ptr: [*]usize = @as([*]usize, @ptrCast(@alignCast(data_ptr))); - return (ptr - 1)[0]; + if (data_ptr) |non_null_ptr| { + const ptr: [*]usize = utils.alignedPtrCast([*]usize, non_null_ptr, @src()); + return (ptr - 1)[0]; + } else { + // This should never happen - indicates corrupted RocStr structure + unreachable; + } } pub fn asSlice(self: *const RocStr) []const u8 { @@ -466,19 +553,19 @@ pub fn init( bytes_ptr: [*]const u8, length: usize, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { return @call(.always_inline, RocStr.init, .{ bytes_ptr, length, roc_ops }); } // Str.equal /// TODO: Document strEqual. -pub fn strEqual(self: RocStr, other: RocStr) callconv(.C) bool { - return self.eq(other); +pub fn strEqual(self: RocStr, other: RocStr) callconv(.c) bool { + return self.eql(other); } // Str.numberOfBytes /// TODO: Document strNumberOfBytes. -pub fn strNumberOfBytes(string: RocStr) callconv(.C) usize { +pub fn strNumberOfBytes(string: RocStr) callconv(.c) usize { return string.len(); } @@ -492,7 +579,7 @@ pub fn exportFromInt( fn func( int: T, roc_ops: *RocOps, - ) callconv(.C) RocStr { + ) callconv(.c) RocStr { return @call(.always_inline, strFromIntHelp, .{ T, int, roc_ops }); } }.func; @@ -531,7 +618,7 @@ pub fn exportFromFloat( fn func( float: T, roc_ops: *RocOps, - ) callconv(.C) RocStr { + ) callconv(.c) RocStr { return @call(.always_inline, strFromFloatHelp, .{ T, float, roc_ops }); } }.func; @@ -556,28 +643,18 @@ pub fn strSplitOn( string: RocStr, delimiter: RocStr, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { const segment_count = countSegments(string, delimiter); const list = RocList.list_allocate(@alignOf(RocStr), segment_count, @sizeOf(RocStr), true, roc_ops); if (list.bytes) |bytes| { - const strings = @as([*]RocStr, @ptrCast(@alignCast(bytes))); + const strings: [*]RocStr = utils.alignedPtrCast([*]RocStr, bytes, @src()); strSplitOnHelp(strings, string, delimiter, roc_ops); } return list; } -fn initFromSmallStr( - slice_bytes: [*]u8, - len: usize, - _: usize, - // TODO we probable don't need this here - roc_ops: *RocOps, -) RocStr { - return RocStr.init(slice_bytes, len, roc_ops); -} - /// TODO pub fn strSplitOnHelp( array: [*]RocStr, @@ -586,7 +663,7 @@ pub fn strSplitOnHelp( roc_ops: *RocOps, ) void { if (delimiter.len() == 0) { - string.incref(1); + string.incref(1, roc_ops); array[0] = string; return; } @@ -604,7 +681,7 @@ pub fn strSplitOnHelp( } // Correct refcount for all of the splits made. - string.incref(i); // i == array.len() + string.incref(i, roc_ops); // i == array.len() } // This is used for `Str.splitOn : Str, Str -> List Str @@ -612,7 +689,7 @@ pub fn strSplitOnHelp( // needs to be broken into, so that we can allocate a array // of that size. It always returns at least 1. /// TODO: Document countSegments. -pub fn countSegments(string: RocStr, delimiter: RocStr) callconv(.C) usize { +pub fn countSegments(string: RocStr, delimiter: RocStr) callconv(.c) usize { if (delimiter.isEmpty()) { return 1; } @@ -626,34 +703,47 @@ pub fn countSegments(string: RocStr, delimiter: RocStr) callconv(.C) usize { } /// TODO: Document countUtf8Bytes. -pub fn countUtf8Bytes(string: RocStr) callconv(.C) u64 { +pub fn countUtf8Bytes(string: RocStr) callconv(.c) u64 { return @intCast(string.len()); } /// TODO: Document isEmpty. -pub fn isEmpty(string: RocStr) callconv(.C) bool { +pub fn isEmpty(string: RocStr) callconv(.c) bool { return string.isEmpty(); } /// TODO: Document getCapacity. -pub fn getCapacity(string: RocStr) callconv(.C) usize { +pub fn getCapacity(string: RocStr) callconv(.c) usize { return string.getCapacity(); } -/// TODO: Document substringUnsafeC. +/// Str.substring - extracts a substring without bounds checking. +/// +/// ## Ownership +/// - `string`: **borrows** - caller retains ownership +/// - Returns: **seamless-slice** - shares data with input string +/// +/// **IMPORTANT**: This function does NOT call incref. The returned seamless +/// slice shares the input's allocation, but the caller is responsible for +/// ensuring the refcount is correct. This is typically used internally where +/// the caller handles refcount management. +/// +/// For small strings: creates a new small string (copy). +/// For heap strings at start=0 with unique refcount: shrinks in place. +/// Otherwise: creates a seamless slice pointing into the original string. pub fn substringUnsafeC( string: RocStr, start_u64: u64, length_u64: u64, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { const start: usize = @intCast(start_u64); const length: usize = @intCast(length_u64); return substringUnsafe(string, start, length, roc_ops); } -/// TODO +/// See substringUnsafeC for ownership documentation. pub fn substringUnsafe( string: RocStr, start: usize, @@ -692,13 +782,13 @@ pub fn substringUnsafe( } /// TODO: Document getUnsafeC. -pub fn getUnsafeC(string: RocStr, index: u64) callconv(.C) u8 { +pub fn getUnsafeC(string: RocStr, index: u64) callconv(.c) u8 { return string.getUnchecked(@intCast(index)); } // Str.startsWith /// TODO: Document startsWith. -pub fn startsWith(string: RocStr, prefix: RocStr) callconv(.C) bool { +pub fn startsWith(string: RocStr, prefix: RocStr) callconv(.c) bool { const bytes_len = string.len(); const bytes_ptr = string.asU8ptr(); @@ -720,13 +810,71 @@ pub fn startsWith(string: RocStr, prefix: RocStr) callconv(.C) bool { return true; } +/// Str.drop_prefix - Returns string with prefix removed, or original if no match. +/// +/// ## Ownership +/// - `string`: **borrows** - caller retains ownership +/// - `prefix`: **borrows** - caller retains ownership +/// - Returns: **seamless-slice** - shares data with input string (incref'd) +/// +/// If prefix doesn't match, returns the original string with refcount incremented. +/// If prefix matches, returns a seamless slice of the remaining portion. +pub fn strDropPrefix( + string: RocStr, + prefix: RocStr, + roc_ops: *RocOps, +) callconv(.c) RocStr { + if (!startsWith(string, prefix)) { + // Prefix doesn't match, return original (with incref) + string.incref(1, roc_ops); + return string; + } + + const prefix_len = prefix.len(); + const new_len = string.len() - prefix_len; + + // Increment refcount for the seamless slice we're about to create. + // This is safe even for small strings (incref is a no-op for them). + string.incref(1, roc_ops); + return substringUnsafe(string, prefix_len, new_len, roc_ops); +} + +/// Str.drop_suffix - Returns string with suffix removed, or original if no match. +/// +/// ## Ownership +/// - `string`: **borrows** - caller retains ownership +/// - `suffix`: **borrows** - caller retains ownership +/// - Returns: **seamless-slice** - shares data with input string (incref'd) +/// +/// If suffix doesn't match, returns the original string with refcount incremented. +/// If suffix matches, returns a seamless slice of the remaining portion. +pub fn strDropSuffix( + string: RocStr, + suffix: RocStr, + roc_ops: *RocOps, +) callconv(.c) RocStr { + if (!endsWith(string, suffix)) { + // Suffix doesn't match, return original (with incref) + string.incref(1, roc_ops); + return string; + } + + const suffix_len = suffix.len(); + const new_len = string.len() - suffix_len; + + // Increment refcount for the seamless slice we're about to create. + // This is safe even for small strings (incref is a no-op for them). + string.incref(1, roc_ops); + return substringUnsafe(string, 0, new_len, roc_ops); +} + // Str.repeat /// TODO: Document repeatC. pub fn repeatC( string: RocStr, count_u64: u64, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { const count: usize = @intCast(count_u64); const bytes_len = string.len(); const bytes_ptr = string.asU8ptr(); @@ -744,7 +892,7 @@ pub fn repeatC( } /// Str.endsWith -pub fn endsWith(string: RocStr, suffix: RocStr) callconv(.C) bool { +pub fn endsWith(string: RocStr, suffix: RocStr) callconv(.c) bool { const bytes_len = string.len(); const bytes_ptr = string.asU8ptr(); @@ -766,16 +914,24 @@ pub fn endsWith(string: RocStr, suffix: RocStr) callconv(.C) bool { return true; } -/// Str.concat +/// Str.concat - concatenates two strings. +/// +/// ## Ownership +/// - `arg1`: **consumes** - may be reallocated if capacity insufficient +/// - `arg2`: **borrows** - caller retains ownership (not decrefd here) +/// - Returns: **independent** or **copy-on-write** depending on arg1's capacity +/// +/// Note: arg1 is owned and may be returned directly if arg2 is empty, +/// or reallocated to accommodate the combined content. pub fn strConcatC( arg1: RocStr, arg2: RocStr, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { return @call(.always_inline, strConcat, .{ arg1, arg2, roc_ops }); } -/// TODO +/// See strConcatC for ownership documentation. pub fn strConcat( arg1: RocStr, arg2: RocStr, @@ -796,6 +952,11 @@ pub fn strConcat( } } +/// Str.contains +pub fn strContains(haystack: RocStr, needle: RocStr) callconv(.c) bool { + return std.mem.indexOf(u8, haystack.asSlice(), needle.asSlice()) != null; +} + /// TODO: Document RocListStr. pub const RocListStr = extern struct { list_elements: ?[*]RocStr, @@ -803,22 +964,37 @@ pub const RocListStr = extern struct { list_capacity_or_alloc_ptr: usize, }; -/// Str.joinWith +/// Str.joinWith - joins a list of strings with a separator. +/// +/// ## Ownership +/// - `list`: **consumes** - elements are borrowed, list is consumed +/// - `separator`: **borrows** - caller retains ownership +/// - Returns: **independent** - new allocation containing joined result pub fn strJoinWithC( list: RocList, separator: RocStr, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { + const list_elements: ?[*]RocStr = if (list.bytes) |bytes| + utils.alignedPtrCast([*]RocStr, bytes, @src()) + else + null; const roc_list_str = RocListStr{ - .list_elements = @as(?[*]RocStr, @ptrCast(@alignCast(list.bytes))), + .list_elements = list_elements, .list_length = list.length, .list_capacity_or_alloc_ptr = list.capacity_or_alloc_ptr, }; - return @call(.always_inline, strJoinWith, .{ roc_list_str, separator, roc_ops }); + const result = @call(.always_inline, strJoinWith, .{ roc_list_str, separator, roc_ops }); + + // Decref the consumed list. Since elements are strings (refcounted), we pass + // elements_refcounted=true and provide strDecref to decref each element. + list.decref(@alignOf(RocStr), @sizeOf(RocStr), true, @ptrCast(roc_ops), &strDecref, roc_ops); + + return result; } -/// TODO +/// See strJoinWithC for ownership documentation. pub fn strJoinWith( list: RocListStr, separator: RocStr, @@ -860,11 +1036,22 @@ pub fn strJoinWith( } } -/// Str.toUtf8 +/// Str.toUtf8 - converts a string to a list of UTF-8 bytes. +/// +/// ## Ownership +/// - `arg`: **borrows** - caller retains ownership +/// - Returns: **seamless-slice** - shares underlying data with input string +/// +/// For heap strings, the returned list shares the same underlying allocation +/// as the input string. This function calls `incref` on the allocation to +/// account for the new reference. Small strings are copied to a new allocation. +/// +/// The caller must decref the argument after call (we borrowed it but added +/// a reference to its data via the returned list). pub fn strToUtf8C( arg: RocStr, roc_ops: *RocOps, -) callconv(.C) RocList { +) callconv(.c) RocList { return strToBytes(arg, roc_ops); } @@ -882,13 +1069,16 @@ inline fn strToBytes( return RocList{ .length = length, .bytes = ptr, .capacity_or_alloc_ptr = length }; } else { + // The returned list shares the same underlying allocation as the string. + // We must incref the allocation since there's now an additional reference to it. + arg.incref(1, roc_ops); const is_seamless_slice = arg.length & SEAMLESS_SLICE_BIT; return RocList{ .length = length, .bytes = arg.bytes, .capacity_or_alloc_ptr = arg.capacity_or_alloc_ptr | is_seamless_slice }; } } /// TODO -pub const FromUtf8Result = extern struct { +pub const FromUtf8Try = extern struct { byte_index: u64, string: RocStr, is_ok: bool, @@ -900,7 +1090,7 @@ pub fn fromUtf8C( list: RocList, update_mode: UpdateMode, roc_ops: *RocOps, -) callconv(.C) FromUtf8Result { +) callconv(.c) FromUtf8Try { return fromUtf8(list, update_mode, roc_ops); } @@ -980,8 +1170,10 @@ fn utf8EncodeLossy(c: u32, out: []u8) u3 { pub fn fromUtf8Lossy( list: RocList, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { if (list.len() == 0) { + // Free the empty list since we consume ownership + list.decref(@alignOf(u8), @sizeOf(u8), false, null, &rcNone, roc_ops); return RocStr.empty(); } @@ -1002,6 +1194,10 @@ pub fn fromUtf8Lossy( end_index += utf8EncodeLossy(c, ptr[end_index..]); } str.setLen(end_index); + + // Free the input list since we consume ownership + list.decref(@alignOf(u8), @sizeOf(u8), false, null, &rcNone, roc_ops); + return str; } @@ -1012,10 +1208,10 @@ pub fn fromUtf8( // TODO seems odd that we need this here // maybe we should pass in undefined or something to list.decref? roc_ops: *RocOps, -) FromUtf8Result { +) FromUtf8Try { if (list.len() == 0) { - list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, roc_ops); - return FromUtf8Result{ + list.decref(@alignOf(u8), @sizeOf(u8), false, null, &rcNone, roc_ops); + return FromUtf8Try{ .is_ok = true, .string = RocStr.empty(), .byte_index = 0, @@ -1026,8 +1222,8 @@ pub fn fromUtf8( if (isValidUnicode(bytes)) { // Make a seamless slice of the input. - const string = RocStr.fromSubListUnsafe(list, 0, list.len(), update_mode); - return FromUtf8Result{ + const string = RocStr.fromSubListUnsafe(list, 0, list.len(), update_mode, roc_ops); + return FromUtf8Try{ .is_ok = true, .string = string, .byte_index = 0, @@ -1036,9 +1232,9 @@ pub fn fromUtf8( } else { const temp = errorToProblem(bytes); - list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, roc_ops); + list.decref(@alignOf(u8), @sizeOf(u8), false, null, &rcNone, roc_ops); - return FromUtf8Result{ + return FromUtf8Try{ .is_ok = false, .string = RocStr.empty(), .byte_index = @intCast(temp.index), @@ -1158,7 +1354,7 @@ pub fn validateUtf8Bytes( bytes: [*]u8, length: usize, roc_ops: *RocOps, -) FromUtf8Result { +) FromUtf8Try { return fromUtf8(RocList{ .bytes = bytes, .length = length, .capacity_or_alloc_ptr = length }, .Immutable, roc_ops); } @@ -1166,7 +1362,7 @@ pub fn validateUtf8Bytes( pub fn validateUtf8BytesX( str: RocList, roc_ops: *RocOps, -) FromUtf8Result { +) FromUtf8Try { return fromUtf8(str, .Immutable, roc_ops); } @@ -1185,8 +1381,8 @@ pub fn sliceHelp( } /// TODO -pub fn toErrUtf8ByteResponse(index: usize, problem: Utf8ByteProblem) FromUtf8Result { - return FromUtf8Result{ .is_ok = false, .string = RocStr.empty(), .byte_index = @as(u64, @intCast(index)), .problem_code = problem }; +pub fn toErrUtf8ByteResponse(index: usize, problem: Utf8ByteProblem) FromUtf8Try { + return FromUtf8Try{ .is_ok = false, .string = RocStr.empty(), .byte_index = @as(u64, @intCast(index)), .problem_code = problem }; } // NOTE on memory: the validate function consumes a RC token of the input. Since @@ -1215,11 +1411,21 @@ pub fn isWhitespace(codepoint: u21) bool { }; } -/// TODO: Document strTrim. +/// Str.trim - removes leading and trailing whitespace. +/// +/// ## Ownership +/// - `input_string`: **consumes** - caller loses ownership +/// - Returns: **copy-on-write** or **seamless-slice** depending on input +/// +/// Behavior depends on input state: +/// - Empty string: returns empty (decrefs input if heap-allocated) +/// - Small string: creates new small string with trimmed bytes +/// - Unique with no leading whitespace: shrinks in place (same allocation) +/// - Otherwise: creates seamless slice pointing to trimmed region pub fn strTrim( input_string: RocStr, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { var string = input_string; if (string.isEmpty()) { @@ -1268,11 +1474,21 @@ pub fn strTrim( } } -/// TODO: Document strTrimStart. +/// Str.trim_start - removes leading whitespace. +/// +/// ## Ownership +/// - `input_string`: **consumes** - caller loses ownership +/// - Returns: **copy-on-write** or **seamless-slice** depending on input +/// +/// Behavior depends on input state: +/// - Empty string: returns empty (decrefs input if heap-allocated) +/// - Small string: creates new small string with trimmed bytes +/// - Unique with no leading whitespace: returns same allocation unchanged +/// - Otherwise: creates seamless slice pointing to trimmed region pub fn strTrimStart( input_string: RocStr, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { var string = input_string; if (string.isEmpty()) { @@ -1320,11 +1536,21 @@ pub fn strTrimStart( } } -/// TODO: Document strTrimEnd. +/// Str.trim_end - removes trailing whitespace. +/// +/// ## Ownership +/// - `input_string`: **consumes** - caller loses ownership +/// - Returns: **copy-on-write** - may be same allocation if unique +/// +/// Behavior depends on input state: +/// - Empty string: returns empty (decrefs input if heap-allocated) +/// - Small string: creates new small string with trimmed bytes +/// - Unique: shrinks length in place (same allocation) +/// - Shared: creates seamless slice pointing to trimmed region pub fn strTrimEnd( input_string: RocStr, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { var string = input_string; if (string.isEmpty()) { @@ -1405,10 +1631,19 @@ fn countTrailingWhitespaceBytes(string: RocStr) usize { } /// Str.with_ascii_lowercased +/// +/// Returns a string with all ASCII letters converted to lowercase. +/// +/// ## Ownership +/// - `string`: **consumes** - caller loses ownership +/// - Returns: **copy-on-write** - may be same allocation if input was unique +/// +/// If the input string is unique, modifies in place and returns it. +/// If shared, decrefs the input and allocates a new string. pub fn strWithAsciiLowercased( string: RocStr, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { var new_str = if (string.isUnique()) string else blk: { @@ -1424,10 +1659,19 @@ pub fn strWithAsciiLowercased( } /// Str.with_ascii_uppercased +/// +/// Returns a string with all ASCII letters converted to uppercase. +/// +/// ## Ownership +/// - `string`: **consumes** - caller loses ownership +/// - Returns: **copy-on-write** - may be same allocation if input was unique +/// +/// If the input string is unique, modifies in place and returns it. +/// If shared, decrefs the input and allocates a new string. pub fn strWithAsciiUppercased( string: RocStr, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { var new_str = if (string.isUnique()) string else blk: { @@ -1443,7 +1687,7 @@ pub fn strWithAsciiUppercased( } /// TODO: Document strCaselessAsciiEquals. -pub fn strCaselessAsciiEquals(self: RocStr, other: RocStr) callconv(.C) bool { +pub fn strCaselessAsciiEquals(self: RocStr, other: RocStr) callconv(.c) bool { if (self.bytes == other.bytes and self.length == other.length) { return true; } @@ -1451,8 +1695,8 @@ pub fn strCaselessAsciiEquals(self: RocStr, other: RocStr) callconv(.C) bool { return ascii.eqlIgnoreCase(self.asSlice(), other.asSlice()); } -fn decStr(ptr: ?[*]u8) callconv(.C) void { - const str_ptr = @as(*RocStr, @ptrCast(@alignCast(ptr orelse unreachable))); +fn decStr(ptr: ?[*]u8) callconv(.c) void { + const str_ptr: *RocStr = utils.alignedPtrCast(*RocStr, ptr orelse unreachable, @src()); str_ptr.decref(); } @@ -1523,7 +1767,7 @@ pub fn reserveC( string: RocStr, spare_u64: u64, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { return reserve(string, @intCast(spare_u64), roc_ops); } @@ -1548,7 +1792,7 @@ pub fn reserve( pub fn withCapacityC( capacity: u64, roc_ops: *RocOps, -) callconv(.C) RocStr { +) callconv(.c) RocStr { var str = RocStr.allocate(@intCast(capacity), roc_ops); str.setLen(0); return str; @@ -1560,7 +1804,7 @@ pub fn strCloneTo( ptr: [*]u8, offset: usize, extra_offset: usize, -) callconv(.C) usize { +) callconv(.c) usize { const WIDTH: usize = @sizeOf(RocStr); if (string.isSmallStr()) { const array: [@sizeOf(RocStr)]u8 = @as([@sizeOf(RocStr)]u8, @bitCast(string)); @@ -1591,7 +1835,7 @@ pub fn strCloneTo( /// Returns a pointer to the allocation backing the given RocStr pub fn strAllocationPtr( string: RocStr, -) callconv(.C) ?[*]u8 { +) callconv(.c) ?[*]u8 { return string.getAllocationPtr(); } @@ -1599,7 +1843,7 @@ pub fn strAllocationPtr( pub fn strReleaseExcessCapacity( roc_ops: *RocOps, string: RocStr, -) callconv(.C) RocStr { +) callconv(.c) RocStr { const old_length = string.len(); // We use the direct list.capacity_or_alloc_ptr to make sure both that there is no extra capacity and that it isn't a seamless slice. if (string.isSmallStr()) { @@ -1622,7 +1866,7 @@ pub fn strReleaseExcessCapacity( } } -fn expectOk(result: FromUtf8Result) !void { +fn expectOk(result: FromUtf8Try) !void { try std.testing.expectEqual(result.is_ok, true); } @@ -1645,7 +1889,7 @@ test "RocStr.eq: small, equal" { const str2_ptr: [*]u8 = &str2; var roc_str2 = RocStr.init(str2_ptr, str2_len, test_env.getOps()); - try std.testing.expect(roc_str1.eq(roc_str2)); + try std.testing.expect(roc_str1.eql(roc_str2)); roc_str1.decref(test_env.getOps()); roc_str2.decref(test_env.getOps()); @@ -1670,7 +1914,7 @@ test "RocStr.eq: small, not equal, different length" { roc_str2.decref(test_env.getOps()); } - try std.testing.expect(!roc_str1.eq(roc_str2)); + try std.testing.expect(!roc_str1.eql(roc_str2)); } test "RocStr.eq: small, not equal, same length" { @@ -1692,7 +1936,7 @@ test "RocStr.eq: small, not equal, same length" { roc_str2.decref(test_env.getOps()); } - try std.testing.expect(!roc_str1.eq(roc_str2)); + try std.testing.expect(!roc_str1.eql(roc_str2)); } test "RocStr.eq: large, equal" { @@ -1708,7 +1952,7 @@ test "RocStr.eq: large, equal" { roc_str2.decref(test_env.getOps()); } - try std.testing.expect(roc_str1.eq(roc_str2)); + try std.testing.expect(roc_str1.eql(roc_str2)); } test "RocStr.eq: large, different lengths, unequal" { @@ -1725,7 +1969,7 @@ test "RocStr.eq: large, different lengths, unequal" { roc_str2.decref(test_env.getOps()); } - try std.testing.expect(!roc_str1.eq(roc_str2)); + try std.testing.expect(!roc_str1.eql(roc_str2)); } test "RocStr.eq: large, different content, unequal" { @@ -1742,7 +1986,7 @@ test "RocStr.eq: large, different content, unequal" { roc_str2.decref(test_env.getOps()); } - try std.testing.expect(!roc_str1.eq(roc_str2)); + try std.testing.expect(!roc_str1.eql(roc_str2)); } test "RocStr.eq: large, garbage after end, equal" { @@ -1765,7 +2009,7 @@ test "RocStr.eq: large, garbage after end, equal" { roc_str2.decref(test_env.getOps()); } - try std.testing.expect(roc_str1.eq(roc_str2)); + try std.testing.expect(roc_str1.eql(roc_str2)); } test "strSplitHelp: empty delimiter" { @@ -1802,7 +2046,7 @@ test "strSplitHelp: empty delimiter" { } try std.testing.expectEqual(array.len, expected.len); - try std.testing.expect(array[0].eq(expected[0])); + try std.testing.expect(array[0].eql(expected[0])); } test "strSplitHelp: no delimiter" { @@ -1839,7 +2083,7 @@ test "strSplitHelp: no delimiter" { } try std.testing.expectEqual(array.len, expected.len); - try std.testing.expect(array[0].eq(expected[0])); + try std.testing.expect(array[0].eql(expected[0])); } test "strSplitHelp: empty start" { @@ -1881,8 +2125,8 @@ test "strSplitHelp: empty start" { } try std.testing.expectEqual(array.len, expected.len); - try std.testing.expect(array[0].eq(expected[0])); - try std.testing.expect(array[1].eq(expected[1])); + try std.testing.expect(array[0].eql(expected[0])); + try std.testing.expect(array[1].eql(expected[1])); } test "strSplitHelp: empty end" { @@ -1926,9 +2170,9 @@ test "strSplitHelp: empty end" { } try std.testing.expectEqual(array.len, expected.len); - try std.testing.expect(array[0].eq(expected[0])); - try std.testing.expect(array[1].eq(expected[1])); - try std.testing.expect(array[2].eq(expected[2])); + try std.testing.expect(array[0].eql(expected[0])); + try std.testing.expect(array[1].eql(expected[1])); + try std.testing.expect(array[2].eql(expected[2])); } test "strSplitHelp: string equals delimiter" { @@ -1962,8 +2206,8 @@ test "strSplitHelp: string equals delimiter" { } try std.testing.expectEqual(array.len, expected.len); - try std.testing.expect(array[0].eq(expected[0])); - try std.testing.expect(array[1].eq(expected[1])); + try std.testing.expect(array[0].eql(expected[0])); + try std.testing.expect(array[1].eql(expected[1])); } test "strSplitHelp: delimiter on sides" { @@ -2006,9 +2250,9 @@ test "strSplitHelp: delimiter on sides" { } try std.testing.expectEqual(array.len, expected.len); - try std.testing.expect(array[0].eq(expected[0])); - try std.testing.expect(array[1].eq(expected[1])); - try std.testing.expect(array[2].eq(expected[2])); + try std.testing.expect(array[0].eql(expected[0])); + try std.testing.expect(array[1].eql(expected[1])); + try std.testing.expect(array[2].eql(expected[2])); } test "strSplitHelp: three pieces" { @@ -2050,9 +2294,9 @@ test "strSplitHelp: three pieces" { } try std.testing.expectEqual(expected_array.len, array.len); - try std.testing.expect(array[0].eq(expected_array[0])); - try std.testing.expect(array[1].eq(expected_array[1])); - try std.testing.expect(array[2].eq(expected_array[2])); + try std.testing.expect(array[0].eql(expected_array[0])); + try std.testing.expect(array[1].eql(expected_array[1])); + try std.testing.expect(array[2].eql(expected_array[2])); } test "strSplitHelp: overlapping delimiter 1" { @@ -2079,8 +2323,8 @@ test "strSplitHelp: overlapping delimiter 1" { // strings are all small so we ignore freeing the memory try std.testing.expectEqual(array.len, expected.len); - try std.testing.expect(array[0].eq(expected[0])); - try std.testing.expect(array[1].eq(expected[1])); + try std.testing.expect(array[0].eql(expected[0])); + try std.testing.expect(array[1].eql(expected[1])); } test "strSplitHelp: overlapping delimiter 2" { @@ -2108,9 +2352,9 @@ test "strSplitHelp: overlapping delimiter 2" { // strings are all small so we ignore freeing the memory try std.testing.expectEqual(array.len, expected.len); - try std.testing.expect(array[0].eq(expected[0])); - try std.testing.expect(array[1].eq(expected[1])); - try std.testing.expect(array[2].eq(expected[2])); + try std.testing.expect(array[0].eql(expected[0])); + try std.testing.expect(array[1].eql(expected[1])); + try std.testing.expect(array[2].eql(expected[2])); } test "countSegments: long delimiter" { @@ -2234,7 +2478,7 @@ test "substringUnsafe: start" { const actual = substringUnsafe(str, 0, 3, test_env.getOps()); - try std.testing.expect(RocStr.eq(actual, expected)); + try std.testing.expect(RocStr.eql(actual, expected)); } test "substringUnsafe: middle" { @@ -2249,7 +2493,7 @@ test "substringUnsafe: middle" { const actual = substringUnsafe(str, 1, 3, test_env.getOps()); - try std.testing.expect(RocStr.eq(actual, expected)); + try std.testing.expect(RocStr.eql(actual, expected)); } test "substringUnsafe: end" { @@ -2264,7 +2508,7 @@ test "substringUnsafe: end" { const actual = substringUnsafe(str, 23, 37 - 23, test_env.getOps()); - try std.testing.expect(RocStr.eq(actual, expected)); + try std.testing.expect(RocStr.eql(actual, expected)); } test "startsWith: food starts with foo" { @@ -2371,7 +2615,7 @@ test "RocStr.concat: small concat small" { defer result.decref(test_env.getOps()); - try std.testing.expect(roc_str3.eq(result)); + try std.testing.expect(roc_str3.eql(result)); } test "RocStr.joinWith: result is big" { @@ -2410,7 +2654,7 @@ test "RocStr.joinWith: result is big" { defer result.decref(test_env.getOps()); - try std.testing.expect(roc_result.eq(result)); + try std.testing.expect(roc_result.eql(result)); } test "validateUtf8Bytes: ascii" { @@ -2482,14 +2726,14 @@ test "fromUtf8Lossy: ascii, emoji" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); - var list = RocList.fromSlice(u8, "r💖c", false, test_env.getOps()); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const list = RocList.fromSlice(u8, "r💖c", false, test_env.getOps()); + // fromUtf8Lossy consumes ownership of the list - no manual decref needed const res = fromUtf8Lossy(list, test_env.getOps()); defer res.decref(test_env.getOps()); const expected = RocStr.fromSlice("r💖c", test_env.getOps()); defer expected.decref(test_env.getOps()); - try std.testing.expect(expected.eq(res)); + try std.testing.expect(expected.eql(res)); } fn expectErr( @@ -2693,56 +2937,56 @@ test "fromUtf8Lossy: invalid start byte" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); - var list = RocList.fromSlice(u8, "r\x80c", false, test_env.getOps()); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const list = RocList.fromSlice(u8, "r\x80c", false, test_env.getOps()); + // fromUtf8Lossy consumes ownership of the list - no manual decref needed const res = fromUtf8Lossy(list, test_env.getOps()); defer res.decref(test_env.getOps()); const expected = RocStr.fromSlice("r�c", test_env.getOps()); defer expected.decref(test_env.getOps()); - try std.testing.expect(expected.eq(res)); + try std.testing.expect(expected.eql(res)); } test "fromUtf8Lossy: overlong encoding" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); - var list = RocList.fromSlice(u8, "r\xF0\x9F\x92\x96\x80c", false, test_env.getOps()); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const list = RocList.fromSlice(u8, "r\xF0\x9F\x92\x96\x80c", false, test_env.getOps()); + // fromUtf8Lossy consumes ownership of the list - no manual decref needed const res = fromUtf8Lossy(list, test_env.getOps()); defer res.decref(test_env.getOps()); const expected = RocStr.fromSlice("r💖�c", test_env.getOps()); defer expected.decref(test_env.getOps()); - try std.testing.expect(expected.eq(res)); + try std.testing.expect(expected.eql(res)); } test "fromUtf8Lossy: expected continuation" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); - var list = RocList.fromSlice(u8, "r\xCFc", false, test_env.getOps()); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const list = RocList.fromSlice(u8, "r\xCFc", false, test_env.getOps()); + // fromUtf8Lossy consumes ownership of the list - no manual decref needed const res = fromUtf8Lossy(list, test_env.getOps()); defer res.decref(test_env.getOps()); const expected = RocStr.fromSlice("r�c", test_env.getOps()); defer expected.decref(test_env.getOps()); - try std.testing.expect(expected.eq(res)); + try std.testing.expect(expected.eql(res)); } test "fromUtf8Lossy: unexpected end" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); - var list = RocList.fromSlice(u8, "r\xCF", false, test_env.getOps()); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const list = RocList.fromSlice(u8, "r\xCF", false, test_env.getOps()); + // fromUtf8Lossy consumes ownership of the list - no manual decref needed const res = fromUtf8Lossy(list, test_env.getOps()); defer res.decref(test_env.getOps()); const expected = RocStr.fromSlice("r�", test_env.getOps()); defer expected.decref(test_env.getOps()); - try std.testing.expect(expected.eq(res)); + try std.testing.expect(expected.eql(res)); } test "fromUtf8Lossy: encodes surrogate" { @@ -2754,14 +2998,14 @@ test "fromUtf8Lossy: encodes surrogate" { // becomes 0b1110_1101 0b10_1000_00 0b10_11_1101 // 1110_wwww 10_xxxx_yy 10_yy_zzzz // 0xED 0x90 0xBD - var list = RocList.fromSlice(u8, "r\xED\xA0\xBDc", false, test_env.getOps()); - defer list.decref(@alignOf(u8), @sizeOf(u8), false, rcNone, test_env.getOps()); + const list = RocList.fromSlice(u8, "r\xED\xA0\xBDc", false, test_env.getOps()); + // fromUtf8Lossy consumes ownership of the list - no manual decref needed const res = fromUtf8Lossy(list, test_env.getOps()); defer res.decref(test_env.getOps()); const expected = RocStr.fromSlice("r�c", test_env.getOps()); defer expected.decref(test_env.getOps()); - try std.testing.expect(expected.eq(res)); + try std.testing.expect(expected.eql(res)); } test "isWhitespace" { @@ -2784,7 +3028,7 @@ test "withAsciiLowercased: small str" { defer str_result.decref(test_env.getOps()); try std.testing.expect(str_result.isSmallStr()); - try std.testing.expect(str_result.eq(expected)); + try std.testing.expect(str_result.eql(expected)); } test "withAsciiLowercased: non small str" { @@ -2801,7 +3045,7 @@ test "withAsciiLowercased: non small str" { const str_result = strWithAsciiLowercased(original, test_env.getOps()); try std.testing.expect(!str_result.isSmallStr()); - try std.testing.expect(str_result.eq(expected)); + try std.testing.expect(str_result.eql(expected)); } test "withAsciiLowercased: seamless slice" { @@ -2820,7 +3064,7 @@ test "withAsciiLowercased: seamless slice" { const str_result = strWithAsciiLowercased(original, test_env.getOps()); try std.testing.expect(!str_result.isSmallStr()); - try std.testing.expect(str_result.eq(expected)); + try std.testing.expect(str_result.eql(expected)); } test "withAsciiUppercased: small str" { @@ -2837,7 +3081,7 @@ test "withAsciiUppercased: small str" { defer str_result.decref(test_env.getOps()); try std.testing.expect(str_result.isSmallStr()); - try std.testing.expect(str_result.eq(expected)); + try std.testing.expect(str_result.eql(expected)); } test "withAsciiUppercased: non small str" { @@ -2854,7 +3098,7 @@ test "withAsciiUppercased: non small str" { const str_result = strWithAsciiUppercased(original, test_env.getOps()); try std.testing.expect(!str_result.isSmallStr()); - try std.testing.expect(str_result.eq(expected)); + try std.testing.expect(str_result.eql(expected)); } test "withAsciiUppercased: seamless slice" { @@ -2873,7 +3117,7 @@ test "withAsciiUppercased: seamless slice" { const str_result = strWithAsciiUppercased(original, test_env.getOps()); try std.testing.expect(!str_result.isSmallStr()); - try std.testing.expect(str_result.eq(expected)); + try std.testing.expect(str_result.eql(expected)); } test "caselessAsciiEquals: same str" { @@ -2955,7 +3199,7 @@ test "strTrim: empty" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); const trimmedEmpty = strTrim(RocStr.empty(), test_env.getOps()); - try std.testing.expect(trimmedEmpty.eq(RocStr.empty())); + try std.testing.expect(trimmedEmpty.eql(RocStr.empty())); } test "strTrim: null byte" { @@ -2977,7 +3221,7 @@ test "strTrim: null byte" { const trimmed = strTrim(original.clone(test_env.getOps()), test_env.getOps()); defer trimmed.decref(test_env.getOps()); - try std.testing.expect(original.eq(trimmed)); + try std.testing.expect(original.eql(trimmed)); } test "strTrim: blank" { @@ -2990,7 +3234,7 @@ test "strTrim: blank" { const trimmed = strTrim(original, test_env.getOps()); defer trimmed.decref(test_env.getOps()); - try std.testing.expect(trimmed.eq(RocStr.empty())); + try std.testing.expect(trimmed.eql(RocStr.empty())); } test "strTrim: large to large" { @@ -3011,7 +3255,7 @@ test "strTrim: large to large" { const trimmed = strTrim(original, test_env.getOps()); defer trimmed.decref(test_env.getOps()); - try std.testing.expect(trimmed.eq(expected)); + try std.testing.expect(trimmed.eql(expected)); } test "strTrim: large to small sized slice" { @@ -3033,7 +3277,7 @@ test "strTrim: large to small sized slice" { const trimmed = strTrim(original, test_env.getOps()); defer trimmed.decref(test_env.getOps()); - try std.testing.expect(trimmed.eq(expected)); + try std.testing.expect(trimmed.eql(expected)); try std.testing.expect(!trimmed.isSmallStr()); } @@ -3055,7 +3299,7 @@ test "strTrim: small to small" { const trimmed = strTrim(original, test_env.getOps()); - try std.testing.expect(trimmed.eq(expected)); + try std.testing.expect(trimmed.eql(expected)); try std.testing.expect(trimmed.isSmallStr()); } @@ -3064,7 +3308,7 @@ test "strTrimStart: empty" { defer test_env.deinit(); const trimmedEmpty = strTrimStart(RocStr.empty(), test_env.getOps()); - try std.testing.expect(trimmedEmpty.eq(RocStr.empty())); + try std.testing.expect(trimmedEmpty.eql(RocStr.empty())); } test "strTrimStart: blank" { @@ -3077,7 +3321,7 @@ test "strTrimStart: blank" { const trimmed = strTrimStart(original, test_env.getOps()); - try std.testing.expect(trimmed.eq(RocStr.empty())); + try std.testing.expect(trimmed.eql(RocStr.empty())); } test "strTrimStart: large to large" { @@ -3098,7 +3342,7 @@ test "strTrimStart: large to large" { const trimmed = strTrimStart(original, test_env.getOps()); - try std.testing.expect(trimmed.eq(expected)); + try std.testing.expect(trimmed.eql(expected)); } test "strTrimStart: large to small" { @@ -3120,7 +3364,7 @@ test "strTrimStart: large to small" { const trimmed = strTrimStart(original, test_env.getOps()); defer trimmed.decref(test_env.getOps()); - try std.testing.expect(trimmed.eq(expected)); + try std.testing.expect(trimmed.eql(expected)); try std.testing.expect(!trimmed.isSmallStr()); } @@ -3142,7 +3386,7 @@ test "strTrimStart: small to small" { const trimmed = strTrimStart(original, test_env.getOps()); - try std.testing.expect(trimmed.eq(expected)); + try std.testing.expect(trimmed.eql(expected)); try std.testing.expect(trimmed.isSmallStr()); } @@ -3150,7 +3394,7 @@ test "strTrimEnd: empty" { var test_env = TestEnv.init(std.testing.allocator); defer test_env.deinit(); const trimmedEmpty = strTrimEnd(RocStr.empty(), test_env.getOps()); - try std.testing.expect(trimmedEmpty.eq(RocStr.empty())); + try std.testing.expect(trimmedEmpty.eql(RocStr.empty())); } test "strTrimEnd: blank" { @@ -3162,7 +3406,7 @@ test "strTrimEnd: blank" { const trimmed = strTrimEnd(original, test_env.getOps()); - try std.testing.expect(trimmed.eq(RocStr.empty())); + try std.testing.expect(trimmed.eql(RocStr.empty())); } test "strTrimEnd: large to large" { @@ -3182,7 +3426,7 @@ test "strTrimEnd: large to large" { const trimmed = strTrimEnd(original, test_env.getOps()); - try std.testing.expect(trimmed.eq(expected)); + try std.testing.expect(trimmed.eql(expected)); } test "strTrimEnd: large to small" { @@ -3204,7 +3448,7 @@ test "strTrimEnd: large to small" { const trimmed = strTrimEnd(original, test_env.getOps()); defer trimmed.decref(test_env.getOps()); - try std.testing.expect(trimmed.eq(expected)); + try std.testing.expect(trimmed.eql(expected)); try std.testing.expect(!trimmed.isSmallStr()); } @@ -3226,7 +3470,7 @@ test "strTrimEnd: small to small" { const trimmed = strTrimEnd(original, test_env.getOps()); - try std.testing.expect(trimmed.eq(expected)); + try std.testing.expect(trimmed.eql(expected)); try std.testing.expect(trimmed.isSmallStr()); } diff --git a/src/builtins/utils.zig b/src/builtins/utils.zig index 482401eb92..6d51f01a64 100644 --- a/src/builtins/utils.zig +++ b/src/builtins/utils.zig @@ -3,6 +3,7 @@ //! This module provides essential infrastructure for builtin operations, //! including memory allocation interfaces, overflow detection utilities, //! debug functions, and common types used throughout the builtin modules. +//! //! It serves as the foundation layer that other builtin modules depend on //! for low-level operations and host interface functions. const std = @import("std"); @@ -16,9 +17,42 @@ const RocDbg = @import("host_abi.zig").RocDbg; const RocExpectFailed = @import("host_abi.zig").RocExpectFailed; const RocCrashed = @import("host_abi.zig").RocCrashed; -const DEBUG_INCDEC = false; const DEBUG_TESTING_ALLOC = false; -const DEBUG_ALLOC = false; + +/// Performs a pointer cast with debug-mode alignment verification. +/// +/// In debug builds, verifies that the pointer is properly aligned for the target type +/// and panics with detailed diagnostic information if alignment is incorrect. +/// In release builds, this is equivalent to `@ptrCast(@alignCast(ptr))`. +/// +/// Usage: +/// ``` +/// const typed_ptr: *usize = alignedPtrCast(*usize, raw_ptr, @src()); +/// ``` +/// +/// The `src` parameter should always be `@src()` at the call site - this captures +/// the file, function, and line number to aid in reproducing alignment bugs. +pub inline fn alignedPtrCast(comptime T: type, ptr: anytype, src: std.builtin.SourceLocation) T { + if (comptime builtin.mode == .Debug) { + const ptr_info = @typeInfo(T); + const alignment = switch (ptr_info) { + .pointer => |p| p.alignment, + else => @compileError("alignedPtrCast target must be a pointer type"), + }; + const ptr_int = @intFromPtr(ptr); + if (alignment > 0 and ptr_int % alignment != 0) { + // Alignment errors indicate a bug in the caller. + // We use unreachable here because: + // 1. We don't have access to roc_ops in this utility function + // 2. This is a debug-only check (comptime builtin.mode == .Debug) + // 3. On non-WASM, this will trigger a trap with a stack trace + // 4. The @src() parameter helps identify the call site in logs + _ = src; // Used for debugging context + unreachable; + } + } + return @ptrCast(@alignCast(ptr)); +} /// Tracks allocations for testing purposes with C ABI compatibility. Uses a single global testing allocator to track allocations. If we need multiple independent allocators we will need to modify this and use comptime. pub const TestEnv = struct { @@ -50,7 +84,7 @@ pub const TestEnv = struct { .roc_dbg = rocDbgFn, .roc_expect_failed = rocExpectFailedFn, .roc_crashed = rocCrashedFn, - .host_fns = undefined, // No host functions in tests + .hosted_fns = .{ .count = 0, .fns = undefined }, // No host functions in tests }; } return &self.ops.?; @@ -69,7 +103,12 @@ pub const TestEnv = struct { 4 => self.allocator.free(@as([]align(4) u8, @alignCast(slice))), 8 => self.allocator.free(@as([]align(8) u8, @alignCast(slice))), 16 => self.allocator.free(@as([]align(16) u8, @alignCast(slice))), - else => @panic("Unsupported alignment in test deallocator cleanup"), + else => { + // Use unreachable since we can't call roc_ops.crash in deinit + // This should never happen in properly written tests + std.debug.print("Unsupported alignment in test deallocator cleanup: {d}\n", .{entry.value_ptr.alignment}); + unreachable; + }, } } @@ -80,19 +119,24 @@ pub const TestEnv = struct { return self.allocation_map.count(); } - fn rocAllocFn(roc_alloc: *RocAlloc, env: *anyopaque) callconv(.C) void { + fn rocAllocFn(roc_alloc: *RocAlloc, env: *anyopaque) callconv(.c) void { const self: *TestEnv = @ptrCast(@alignCast(env)); // Allocate memory using the testing allocator with comptime alignment const ptr = switch (roc_alloc.alignment) { - 1 => self.allocator.alignedAlloc(u8, 1, roc_alloc.length), - 2 => self.allocator.alignedAlloc(u8, 2, roc_alloc.length), - 4 => self.allocator.alignedAlloc(u8, 4, roc_alloc.length), - 8 => self.allocator.alignedAlloc(u8, 8, roc_alloc.length), - 16 => self.allocator.alignedAlloc(u8, 16, roc_alloc.length), - else => @panic("Unsupported alignment in test allocator"), + 1 => self.allocator.alignedAlloc(u8, std.mem.Alignment.@"1", roc_alloc.length), + 2 => self.allocator.alignedAlloc(u8, std.mem.Alignment.@"2", roc_alloc.length), + 4 => self.allocator.alignedAlloc(u8, std.mem.Alignment.@"4", roc_alloc.length), + 8 => self.allocator.alignedAlloc(u8, std.mem.Alignment.@"8", roc_alloc.length), + 16 => self.allocator.alignedAlloc(u8, std.mem.Alignment.@"16", roc_alloc.length), + else => { + // Use unreachable since we can't call roc_ops.crash in test allocator + std.debug.print("Unsupported alignment in test allocator: {d}\n", .{roc_alloc.alignment}); + unreachable; + }, } catch { - @panic("Test allocation failed"); + std.debug.print("Test allocation failed\n", .{}); + unreachable; }; // Cast the pointer to *anyopaque @@ -104,13 +148,14 @@ pub const TestEnv = struct { .alignment = roc_alloc.alignment, }) catch { self.allocator.free(ptr); - @panic("Failed to track test allocation"); + std.debug.print("Failed to track test allocation\n", .{}); + unreachable; }; roc_alloc.answer = result; } - fn rocDeallocFn(roc_dealloc: *RocDealloc, env: *anyopaque) callconv(.C) void { + fn rocDeallocFn(roc_dealloc: *RocDealloc, env: *anyopaque) callconv(.c) void { const self: *TestEnv = @ptrCast(@alignCast(env)); if (self.allocation_map.fetchRemove(roc_dealloc.ptr)) |entry| { @@ -123,33 +168,65 @@ pub const TestEnv = struct { 4 => self.allocator.free(@as([]align(4) u8, @alignCast(slice))), 8 => self.allocator.free(@as([]align(8) u8, @alignCast(slice))), 16 => self.allocator.free(@as([]align(16) u8, @alignCast(slice))), - else => @panic("Unsupported alignment in test deallocator"), + else => { + std.debug.print("Unsupported alignment in test deallocator: {d}\n", .{entry.value.alignment}); + unreachable; + }, } } } - fn rocReallocFn(roc_realloc: *RocRealloc, env: *anyopaque) callconv(.C) void { - _ = env; - _ = roc_realloc; - @panic("Test realloc not implemented yet"); + fn rocReallocFn(roc_realloc: *RocRealloc, env: *anyopaque) callconv(.c) void { + const self: *TestEnv = @ptrCast(@alignCast(env)); + + // Look up the old allocation + if (self.allocation_map.fetchRemove(roc_realloc.answer)) |entry| { + const old_bytes: [*]u8 = @ptrCast(@alignCast(roc_realloc.answer)); + const old_slice = old_bytes[0..entry.value.size]; + + // Reallocate with the same alignment + const new_ptr = switch (entry.value.alignment) { + 1 => self.allocator.realloc(old_slice, roc_realloc.new_length), + 2 => self.allocator.realloc(@as([]align(2) u8, @alignCast(old_slice)), roc_realloc.new_length), + 4 => self.allocator.realloc(@as([]align(4) u8, @alignCast(old_slice)), roc_realloc.new_length), + 8 => self.allocator.realloc(@as([]align(8) u8, @alignCast(old_slice)), roc_realloc.new_length), + 16 => self.allocator.realloc(@as([]align(16) u8, @alignCast(old_slice)), roc_realloc.new_length), + else => { + std.debug.print("Unsupported alignment in test reallocator: {d}\n", .{entry.value.alignment}); + unreachable; + }, + } catch { + std.debug.print("Test reallocation failed\n", .{}); + unreachable; + }; + + const result: *anyopaque = @ptrCast(new_ptr.ptr); + + // Update the allocation map with the new pointer and size + self.allocation_map.put(result, AllocationInfo{ + .size = roc_realloc.new_length, + .alignment = entry.value.alignment, + }) catch { + self.allocator.free(new_ptr); + std.debug.print("Failed to track test reallocation\n", .{}); + unreachable; + }; + + roc_realloc.answer = result; + } else { + std.debug.print("Test realloc: pointer not found in allocation map\n", .{}); + unreachable; + } } - fn rocDbgFn(roc_dbg: *const RocDbg, env: *anyopaque) callconv(.C) void { - _ = env; - const message = roc_dbg.utf8_bytes[0..roc_dbg.len]; - std.debug.print("DBG: {s}\n", .{message}); - } + fn rocDbgFn(_: *const RocDbg, _: *anyopaque) callconv(.c) void {} - fn rocExpectFailedFn(roc_expect: *const RocExpectFailed, env: *anyopaque) callconv(.C) void { - _ = env; - const message = @as([*]u8, @ptrCast(roc_expect.utf8_bytes))[0..roc_expect.len]; - std.debug.print("EXPECT FAILED: {s}\n", .{message}); - } + fn rocExpectFailedFn(_: *const RocExpectFailed, _: *anyopaque) callconv(.c) void {} - fn rocCrashedFn(roc_crashed: *const RocCrashed, env: *anyopaque) callconv(.C) noreturn { - _ = env; + fn rocCrashedFn(roc_crashed: *const RocCrashed, _: *anyopaque) callconv(.c) noreturn { const message = roc_crashed.utf8_bytes[0..roc_crashed.len]; - @panic(message); + std.debug.print("Roc crashed: {s}\n", .{message}); + unreachable; } }; @@ -160,11 +237,11 @@ pub fn WithOverflow(comptime T: type) type { } /// Function type for incrementing reference count -pub const Inc = fn (?[*]u8) callconv(.C) void; +pub const Inc = fn (?[*]u8) callconv(.c) void; /// Function type for incrementing reference count by a specific amount -pub const IncN = fn (?[*]u8, u64) callconv(.C) void; +pub const IncN = fn (?[*]u8, u64) callconv(.c) void; /// Function type for decrementing reference count -pub const Dec = fn (?[*]u8) callconv(.C) void; +pub const Dec = fn (?[*]u8) callconv(.c) void; /// Special refcount value that marks data with whole-program lifetime. /// When a refcount equals this value, it indicates static/constant data that should /// never be decremented or freed. This is used for string literals, constant data, @@ -175,6 +252,16 @@ pub const Dec = fn (?[*]u8) callconv(.C) void; /// - It makes the "constant" check very efficient /// - It's safe since normal refcounts should never reach 0 while still being referenced pub const REFCOUNT_STATIC_DATA: isize = 0; + +/// Sentinel value written to freed refcount slots to detect use-after-free. +/// When memory is freed in debug mode, the refcount slot is poisoned with this value. +/// Any subsequent attempt to incref/decref this memory will trigger a panic. +/// This value is only used in debug builds and has zero overhead in release. +/// Uses a recognizable pattern that works on both 32-bit and 64-bit platforms. +const POISON_VALUE: isize = @bitCast(if (@sizeOf(usize) == 8) + @as(usize, 0xDEADBEEFDEADBEEF) +else + @as(usize, 0xDEADBEEF)); /// No-op reference count decrement function. /// Used as a callback when elements don't contain refcounted data or in testing scenarios /// where reference counting operations should be skipped. Matches the `Dec` function type @@ -184,7 +271,7 @@ pub const REFCOUNT_STATIC_DATA: isize = 0; /// - Testing with simple data types that don't need reference counting /// - Working with primitive types that don't contain pointers to refcounted data /// - As a placeholder when the decrement operation is handled elsewhere -pub fn rcNone(_: ?[*]u8) callconv(.C) void {} +pub fn rcNone(_: ?*anyopaque, _: ?[*]u8) callconv(.c) void {} /// Enum representing different integer widths and signedness for runtime type information pub const IntWidth = enum(u8) { @@ -209,27 +296,29 @@ const Refcount = enum { const RC_TYPE: Refcount = .atomic; /// Increments reference count of an RC pointer by specified amount -pub fn increfRcPtrC(ptr_to_refcount: *isize, amount: isize) callconv(.C) void { +pub fn increfRcPtrC(ptr_to_refcount: *isize, amount: isize, roc_ops: *RocOps) callconv(.c) void { if (RC_TYPE == .none) return; - if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { - std.debug.print("| increment {*}: ", .{ptr_to_refcount}); - } - // Ensure that the refcount is not whole program lifetime. const refcount: isize = ptr_to_refcount.*; + + // Debug-only assertions to catch refcount bugs early. + if (builtin.mode == .Debug) { + if (refcount == POISON_VALUE) { + roc_ops.crash("Use-after-free: incref on already-freed memory"); + return; + } + if (refcount <= 0 and !rcConstant(refcount)) { + roc_ops.crash("Invalid incref: incrementing non-positive refcount"); + return; + } + } + if (!rcConstant(refcount)) { // Note: we assume that a refcount will never overflow. // As such, we do not need to cap incrementing. switch (RC_TYPE) { .normal => { - if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { - const old = @as(usize, @bitCast(refcount)); - const new = old + @as(usize, @intCast(amount)); - - std.debug.print("{} + {} = {}!\n", .{ old, amount, new }); - } - ptr_to_refcount.* = refcount +% amount; }, .atomic => { @@ -246,7 +335,7 @@ pub fn decrefRcPtrC( alignment: u32, elements_refcounted: bool, roc_ops: *RocOps, -) callconv(.C) void { +) callconv(.c) void { // IMPORTANT: bytes_or_null is this case is expected to be a pointer to the refcount // (NOT the start of the data, or the start of the allocation) @@ -261,14 +350,17 @@ pub fn decrefRcPtrC( } /// Safely decrements reference count for a potentially null pointer +/// WARNING: This function assumes `bytes` points to 8-byte aligned data. +/// It should NOT be used for seamless slices with non-zero start offsets, +/// as those have misaligned bytes pointers. Use RocList.decref instead. pub fn decrefCheckNullC( bytes_or_null: ?[*]u8, alignment: u32, elements_refcounted: bool, roc_ops: *RocOps, -) callconv(.C) void { +) callconv(.c) void { if (bytes_or_null) |bytes| { - const isizes: [*]isize = @as([*]isize, @ptrCast(@alignCast(bytes))); + const isizes: [*]isize = alignedPtrCast([*]isize, bytes, @src()); return @call( .always_inline, decref_ptr_to_refcount, @@ -285,13 +377,35 @@ pub fn decrefDataPtrC( alignment: u32, elements_refcounted: bool, roc_ops: *RocOps, -) callconv(.C) void { +) callconv(.c) void { const bytes = bytes_or_null orelse return; const data_ptr = @intFromPtr(bytes); + + // Verify original pointer is properly aligned + // Use roc_ops.crash() instead of std.debug.panic for WASM compatibility + if (comptime builtin.mode == .Debug) { + if (data_ptr % @alignOf(usize) != 0) { + var buf: [128]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, "decrefDataPtrC: data_ptr=0x{x} not {d}-byte aligned", .{ data_ptr, @alignOf(usize) }) catch "decrefDataPtrC: alignment error"; + roc_ops.crash(msg); + return; + } + } + const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11; const unmasked_ptr = data_ptr & ~tag_mask; + // Verify alignment before @ptrFromInt + if (comptime builtin.mode == .Debug) { + if (unmasked_ptr % @alignOf(isize) != 0) { + var buf: [128]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, "decrefDataPtrC: unmasked=0x{x} (data=0x{x}) not {d}-byte aligned", .{ unmasked_ptr, data_ptr, @alignOf(isize) }) catch "decrefDataPtrC: unmasked alignment error"; + roc_ops.crash(msg); + return; + } + } + const isizes: [*]isize = @as([*]isize, @ptrFromInt(unmasked_ptr)); const rc_ptr = isizes - 1; @@ -304,16 +418,39 @@ pub fn decrefDataPtrC( pub fn increfDataPtrC( bytes_or_null: ?[*]u8, inc_amount: isize, -) callconv(.C) void { + roc_ops: *RocOps, +) callconv(.c) void { const bytes = bytes_or_null orelse return; const ptr = @intFromPtr(bytes); + + // Verify original pointer is properly aligned (can fail if seamless slice encoding produces bad pointer) + if (comptime builtin.mode == .Debug) { + if (ptr % @alignOf(usize) != 0) { + var buf: [128]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, "increfDataPtrC: ORIGINAL ptr=0x{x} is not {d}-byte aligned", .{ ptr, @alignOf(usize) }) catch "increfDataPtrC: original alignment error"; + roc_ops.crash(msg); + return; + } + } + const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11; const masked_ptr = ptr & ~tag_mask; + const rc_addr = masked_ptr - @sizeOf(usize); - const isizes: *isize = @as(*isize, @ptrFromInt(masked_ptr - @sizeOf(usize))); + // Verify alignment before @ptrFromInt + if (comptime builtin.mode == .Debug) { + if (rc_addr % @alignOf(isize) != 0) { + var buf: [128]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, "increfDataPtrC: rc_addr=0x{x} (ptr=0x{x}, masked=0x{x}) is not {d}-byte aligned", .{ rc_addr, ptr, masked_ptr, @alignOf(isize) }) catch "increfDataPtrC: rc_addr alignment error"; + roc_ops.crash(msg); + return; + } + } - return increfRcPtrC(isizes, inc_amount); + const isizes: *isize = @as(*isize, @ptrFromInt(rc_addr)); + + return increfRcPtrC(isizes, inc_amount, roc_ops); } /// Frees memory for a data pointer regardless of reference count @@ -324,13 +461,23 @@ pub fn freeDataPtrC( alignment: u32, elements_refcounted: bool, roc_ops: *RocOps, -) callconv(.C) void { +) callconv(.c) void { const bytes = bytes_or_null orelse return; const ptr = @intFromPtr(bytes); const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11; const masked_ptr = ptr & ~tag_mask; + // Verify alignment before @ptrFromInt + if (comptime builtin.mode == .Debug) { + if (masked_ptr % @alignOf(isize) != 0) { + var buf: [128]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, "freeDataPtrC: masked_ptr=0x{x} (ptr=0x{x}) is not {d}-byte aligned", .{ masked_ptr, ptr, @alignOf(isize) }) catch "freeDataPtrC: alignment error"; + roc_ops.crash(msg); + return; + } + } + const isizes: [*]isize = @as([*]isize, @ptrFromInt(masked_ptr)); // we always store the refcount right before the data @@ -345,7 +492,7 @@ pub fn freeRcPtrC( alignment: u32, elements_refcounted: bool, roc_ops: *RocOps, -) callconv(.C) void { +) callconv(.c) void { const bytes = bytes_or_null orelse return; return free_ptr_to_refcount(bytes, alignment, elements_refcounted, roc_ops); } @@ -364,34 +511,40 @@ pub fn decref( const bytes = bytes_or_null orelse return; - const isizes: [*]isize = @as([*]isize, @ptrCast(@alignCast(bytes))); + const isizes: [*]isize = alignedPtrCast([*]isize, bytes, @src()); decref_ptr_to_refcount(isizes - 1, alignment, elements_refcounted, roc_ops); } inline fn free_ptr_to_refcount( refcount_ptr: [*]isize, - alignment: u32, + element_alignment: u32, elements_refcounted: bool, roc_ops: *RocOps, ) void { if (RC_TYPE == .none) return; + + // Debug-only: Poison the refcount slot before freeing to detect use-after-free. + // Any subsequent access to this refcount will see POISON_VALUE and panic. + if (builtin.mode == .Debug) { + refcount_ptr[0] = POISON_VALUE; + } + const ptr_width = @sizeOf(usize); const required_space: usize = if (elements_refcounted) (2 * ptr_width) else ptr_width; - const extra_bytes = @max(required_space, alignment); + const extra_bytes = @max(required_space, element_alignment); const allocation_ptr = @as([*]u8, @ptrCast(refcount_ptr)) - (extra_bytes - @sizeOf(usize)); + // Use the same alignment calculation as allocateWithRefcount + const allocation_alignment = @max(ptr_width, element_alignment); + var roc_dealloc_args = RocDealloc{ - .alignment = alignment, + .alignment = allocation_alignment, .ptr = allocation_ptr, }; // NOTE: we don't even check whether the refcount is "infinity" here! roc_ops.roc_dealloc(&roc_dealloc_args, roc_ops.env); - - if (DEBUG_ALLOC and builtin.target.cpu.arch != .wasm32) { - std.debug.print("💀 freed {*}\n", .{allocation_ptr}); - } } inline fn decref_ptr_to_refcount( @@ -402,26 +555,29 @@ inline fn decref_ptr_to_refcount( ) void { if (RC_TYPE == .none) return; - if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { - std.debug.print("| decrement {*}: ", .{refcount_ptr}); - } - // Due to RC alignment tmust take into account pointer size. const ptr_width = @sizeOf(usize); const alignment = @max(ptr_width, element_alignment); // Ensure that the refcount is not whole program lifetime. const refcount: isize = refcount_ptr[0]; + + // Debug-only assertions to catch refcount bugs early. + // Use roc_ops.crash() instead of @panic for WASM compatibility. + if (builtin.mode == .Debug) { + if (refcount == POISON_VALUE) { + roc_ops.crash("Use-after-free: decref on already-freed memory"); + return; + } + if (refcount <= 0 and !rcConstant(refcount)) { + roc_ops.crash("Refcount underflow: decrementing non-positive refcount"); + return; + } + } + if (!rcConstant(refcount)) { switch (RC_TYPE) { .normal => { - if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { - const old = @as(usize, @bitCast(refcount)); - const new = @as(usize, @bitCast(refcount_ptr[0] -% 1)); - - std.debug.print("{} - 1 = {}!\n", .{ old, new }); - } - refcount_ptr[0] = refcount -% 1; if (refcount == 1) { free_ptr_to_refcount(refcount_ptr, alignment, elements_refcounted, roc_ops); @@ -443,21 +599,28 @@ inline fn decref_ptr_to_refcount( /// Handles tag bits in the pointer and extracts the reference count pub fn isUnique( bytes_or_null: ?[*]u8, -) callconv(.C) bool { + roc_ops: *RocOps, +) callconv(.c) bool { const bytes = bytes_or_null orelse return true; const ptr = @intFromPtr(bytes); const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11; const masked_ptr = ptr & ~tag_mask; + // Verify alignment before @ptrFromInt + if (comptime builtin.mode == .Debug) { + if (masked_ptr % @alignOf(isize) != 0) { + var buf: [128]u8 = undefined; + const msg = std.fmt.bufPrint(&buf, "isUnique: masked_ptr=0x{x} (ptr=0x{x}) is not {d}-byte aligned", .{ masked_ptr, ptr, @alignOf(isize) }) catch "isUnique: alignment error"; + roc_ops.crash(msg); + return false; + } + } + const isizes: [*]isize = @as([*]isize, @ptrFromInt(masked_ptr)); const refcount = (isizes - 1)[0]; - if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { - std.debug.print("| is unique {*}\n", .{isizes - 1}); - } - return rcUnique(refcount); } @@ -493,6 +656,28 @@ pub inline fn rcConstant(refcount: isize) bool { } } +/// Debug-only assertion that a data pointer has a valid refcount. +/// Panics if the refcount is poisoned (use-after-free) or invalid (underflow). +/// Compiles to nothing in release builds - zero overhead. +/// +/// Use this at key points in slice-creating or refcount-manipulating functions +/// to catch bugs early during development. +pub inline fn assertValidRefcount(data_ptr: ?[*]u8, roc_ops: *RocOps) void { + if (builtin.mode != .Debug) return; + if (data_ptr) |ptr| { + const rc_ptr: [*]isize = alignedPtrCast([*]isize, ptr - @sizeOf(usize), @src()); + const rc = rc_ptr[0]; + if (rc == POISON_VALUE) { + roc_ops.crash("assertValidRefcount: Use-after-free detected"); + return; + } + if (rc <= 0 and !rcConstant(rc)) { + roc_ops.crash("assertValidRefcount: Invalid refcount (underflow or corruption)"); + return; + } + } +} + // We follow roughly the [fbvector](https://github.com/facebook/folly/blob/main/folly/docs/FBVector.md) when it comes to growing a RocList. // Here is [their growth strategy](https://github.com/facebook/folly/blob/3e0525988fd444201b19b76b390a5927c15cb697/folly/FBVector.h#L1128) for push_back: // @@ -552,7 +737,7 @@ pub fn allocateWithRefcountC( element_alignment: u32, elements_refcounted: bool, roc_ops: *RocOps, -) callconv(.C) [*]u8 { +) callconv(.c) [*]u8 { return allocateWithRefcount(data_bytes, element_alignment, elements_refcounted, roc_ops); } @@ -583,12 +768,9 @@ pub fn allocateWithRefcount( const new_bytes = @as([*]u8, @ptrCast(roc_alloc_args.answer)); - if (DEBUG_ALLOC and builtin.target.cpu.arch != .wasm32) { - std.debug.print("+ allocated {*} ({} bytes with alignment {})\n", .{ new_bytes, data_bytes, alignment }); - } - const data_ptr = new_bytes + extra_bytes; - const refcount_ptr = @as([*]usize, @ptrCast(@as([*]align(ptr_width) u8, @alignCast(data_ptr)) - ptr_width)); + + const refcount_ptr: [*]usize = alignedPtrCast([*]usize, data_ptr - @sizeOf(usize), @src()); refcount_ptr[0] = if (RC_TYPE == .none) REFCOUNT_STATIC_DATA else 1; return data_ptr; @@ -605,15 +787,16 @@ pub const CSlice = extern struct { /// Returns a pointer to the data portion, not the allocation start pub fn unsafeReallocate( source_ptr: [*]u8, - alignment: u32, + element_alignment: u32, old_length: usize, new_length: usize, element_width: usize, elements_refcounted: bool, + roc_ops: *RocOps, ) [*]u8 { const ptr_width: usize = @sizeOf(usize); const required_space: usize = if (elements_refcounted) (2 * ptr_width) else ptr_width; - const extra_bytes = @max(required_space, alignment); + const extra_bytes = @max(required_space, element_alignment); const old_width = extra_bytes + old_length * element_width; const new_width = extra_bytes + new_length * element_width; @@ -624,12 +807,17 @@ pub fn unsafeReallocate( const old_allocation = source_ptr - extra_bytes; - const roc_realloc_args = RocRealloc{ - .alignment = alignment, + // Use the same alignment calculation as allocateWithRefcount + const allocation_alignment = @max(ptr_width, element_alignment); + + var roc_realloc_args = RocRealloc{ + .alignment = allocation_alignment, .new_length = new_width, .answer = old_allocation, }; + roc_ops.roc_realloc(&roc_realloc_args, roc_ops.env); + const new_source = @as([*]u8, @ptrCast(roc_realloc_args.answer)) + extra_bytes; return new_source; } @@ -656,21 +844,27 @@ pub const UpdateMode = enum(u8) { /// /// Note: On most operating systems, this will be affected by ASLR and different each run. /// In WebAssembly, the value will be constant for the entire build. -pub fn dictPseudoSeed() callconv(.C) u64 { +pub fn dictPseudoSeed() callconv(.c) u64 { return @as(u64, @intCast(@intFromPtr(&dictPseudoSeed))); } test "increfC, refcounted data" { + var test_env = TestEnv.init(std.testing.allocator); + defer test_env.deinit(); + var mock_rc: isize = 17; const ptr_to_refcount: *isize = &mock_rc; - @import("utils.zig").increfRcPtrC(ptr_to_refcount, 2); + @import("utils.zig").increfRcPtrC(ptr_to_refcount, 2, test_env.getOps()); try std.testing.expectEqual(mock_rc, 19); } test "increfC, static data" { + var test_env = TestEnv.init(std.testing.allocator); + defer test_env.deinit(); + var mock_rc: isize = @import("utils.zig").REFCOUNT_STATIC_DATA; const ptr_to_refcount: *isize = &mock_rc; - @import("utils.zig").increfRcPtrC(ptr_to_refcount, 2); + @import("utils.zig").increfRcPtrC(ptr_to_refcount, 2, test_env.getOps()); try std.testing.expectEqual(mock_rc, @import("utils.zig").REFCOUNT_STATIC_DATA); } @@ -701,10 +895,9 @@ test "TestEnv basic functionality" { // Should start with no allocations try std.testing.expectEqual(@as(usize, 0), test_env.getAllocationCount()); - // Get ops should work + // Get ops should work - verify we can get ops and it points back to our test env const ops = test_env.getOps(); - // Function pointers are non-null by design, just verify we can get ops - _ = ops; + try std.testing.expectEqual(@as(*anyopaque, @ptrCast(&test_env)), ops.env); } test "TestEnv allocation tracking" { @@ -771,19 +964,19 @@ test "isUnique with different scenarios" { const ops = test_env.getOps(); // Test with null (should return true) - try std.testing.expect(@import("utils.zig").isUnique(null)); + try std.testing.expect(@import("utils.zig").isUnique(null, ops)); // Test with allocated memory const ptr = allocateWithRefcount(64, 8, false, ops); - try std.testing.expect(@import("utils.zig").isUnique(ptr)); + try std.testing.expect(@import("utils.zig").isUnique(ptr, ops)); } test "rcNone function" { // rcNone should be safe to call with any pointer - @import("utils.zig").rcNone(null); + @import("utils.zig").rcNone(null, null); var dummy: u8 = 42; - @import("utils.zig").rcNone(@as(?[*]u8, @ptrCast(&dummy))); + @import("utils.zig").rcNone(null, @as(?[*]u8, @ptrCast(&dummy))); // If we get here without crashing, the test passed try std.testing.expect(true); diff --git a/src/bundle/bundle.zig b/src/bundle/bundle.zig index 86027be295..f62af307ea 100644 --- a/src/bundle/bundle.zig +++ b/src/bundle/bundle.zig @@ -34,7 +34,7 @@ const TAR_EXTENSION = ".tar.zst"; pub const DEFAULT_COMPRESSION_LEVEL: c_int = 22; /// Custom allocator function for zstd that adds extra bytes to store allocation size -pub fn allocForZstd(opaque_ptr: ?*anyopaque, size: usize) callconv(.C) ?*anyopaque { +pub fn allocForZstd(opaque_ptr: ?*anyopaque, size: usize) callconv(.c) ?*anyopaque { const allocator = @as(*std.mem.Allocator, @ptrCast(@alignCast(opaque_ptr.?))); // Allocate extra bytes to store the size const total_size = size + SIZE_STORAGE_BYTES; @@ -49,7 +49,7 @@ pub fn allocForZstd(opaque_ptr: ?*anyopaque, size: usize) callconv(.C) ?*anyopaq } /// Custom free function for zstd that retrieves the original allocation size -pub fn freeForZstd(opaque_ptr: ?*anyopaque, address: ?*anyopaque) callconv(.C) void { +pub fn freeForZstd(opaque_ptr: ?*anyopaque, address: ?*anyopaque) callconv(.c) void { if (address == null) return; const allocator = @as(*std.mem.Allocator, @ptrCast(@alignCast(opaque_ptr.?))); @@ -122,20 +122,16 @@ pub fn bundle( file_path_iter: anytype, compression_level: c_int, allocator: *std.mem.Allocator, - output_writer: anytype, + output_writer: *std.Io.Writer, base_dir: std.fs.Dir, path_prefix: ?[]const u8, error_context: ?*ErrorContext, ) BundleError![]u8 { - // Create a buffered writer for the output - var buffered_writer = std.io.bufferedWriter(output_writer); - const buffered = buffered_writer.writer(); - // Create compressing hash writer that chains: tar → compress → hash → output var compress_writer = streaming_writer.CompressingHashWriter.init( allocator, compression_level, - buffered.any(), + output_writer, allocForZstd, freeForZstd, ) catch |err| switch (err) { @@ -144,7 +140,7 @@ pub fn bundle( defer compress_writer.deinit(); // Create tar writer that writes to the compressing writer - var tar_writer = std.tar.writer(compress_writer.writer()); + var tar_writer = std.tar.Writer{ .underlying_writer = &compress_writer.interface }; // Process files one at a time while (try file_path_iter.next()) |file_path| { @@ -211,31 +207,22 @@ pub fn bundle( }; // Create a reader for the file - const file_reader = file.reader(); + var reader_buffer: [4096]u8 = undefined; + var file_reader = file.reader(&reader_buffer); // Stream the file to tar - tar_writer.writeFileStream(tar_path, file_size, file_reader, options) catch |err| switch (err) { - error.OutOfMemory => return error.OutOfMemory, - else => return error.TarWriteFailed, + tar_writer.writeFileStream(tar_path, file_size, &file_reader.interface, options) catch { + return error.TarWriteFailed; }; } // Finish the tar archive - tar_writer.finish() catch { + tar_writer.finishPedantically() catch { return error.TarWriteFailed; }; - // Finish compression - compress_writer.finish() catch |err| switch (err) { - error.CompressionFailed => return error.CompressionFailed, - error.WriteFailed => return error.WriteFailed, - error.AlreadyFinished => return error.CompressionFailed, - error.OutOfMemory => return error.OutOfMemory, - }; - - buffered_writer.flush() catch { - return error.FlushFailed; - }; + // Finish compression, also flushes the writer + compress_writer.finish() catch return error.WriteFailed; // Get the blake3 hash and encode as base58 const hash = compress_writer.getHash(); @@ -462,17 +449,64 @@ pub fn pathHasUnbundleErr(path: []const u8) ?PathValidationError { pub const ExtractWriter = struct { ptr: *anyopaque, makeDirFn: *const fn (ptr: *anyopaque, path: []const u8) anyerror!void, - streamFileFn: *const fn (ptr: *anyopaque, path: []const u8, reader: std.io.AnyReader, size: usize) anyerror!void, + streamFileFn: *const fn (ptr: *anyopaque, path: []const u8, reader: *std.Io.Reader, size: usize) anyerror!void, pub fn makeDir(self: ExtractWriter, path: []const u8) !void { return self.makeDirFn(self.ptr, path); } - pub fn streamFile(self: ExtractWriter, path: []const u8, reader: std.io.AnyReader, size: usize) !void { + pub fn streamFile(self: ExtractWriter, path: []const u8, reader: *std.Io.Reader, size: usize) !void { return self.streamFileFn(self.ptr, path, reader, size); } }; +const TarEntryReader = struct { + iterator: *std.tar.Iterator, + remaining: u64, + interface: std.Io.Reader, + + fn init(iterator: *std.tar.Iterator, remaining: u64) TarEntryReader { + var result: TarEntryReader = .{ + .iterator = iterator, + .remaining = remaining, + .interface = undefined, + }; + result.interface = .{ + .vtable = &.{ + .stream = stream, + }, + .buffer = &.{}, // No buffer needed, we delegate to iterator.reader + .seek = 0, + .end = 0, + }; + return result; + } + + fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { + const self: *TarEntryReader = @alignCast(@fieldParentPtr("interface", r)); + + if (self.remaining == 0) { + return std.Io.Reader.StreamError.EndOfStream; + } + + const dest = limit.slice(try w.writableSliceGreedy(1)); + const max_bytes = std.math.cast(usize, self.remaining) orelse std.math.maxInt(usize); + const read_limit = @min(dest.len, max_bytes); + const slice = dest[0..read_limit]; + + const bytes_read = self.iterator.reader.readSliceShort(slice) catch |err| switch (err) { + error.ReadFailed => return std.Io.Reader.StreamError.ReadFailed, + }; + + if (bytes_read == 0) return std.Io.Reader.StreamError.EndOfStream; + + self.remaining -= bytes_read; + self.iterator.unread_file_bytes = self.remaining; + w.advance(bytes_read); + return bytes_read; + } +}; + /// Directory-based extract writer pub const DirExtractWriter = struct { dir: std.fs.Dir, @@ -494,7 +528,7 @@ pub const DirExtractWriter = struct { try self.dir.makePath(path); } - fn streamFile(ptr: *anyopaque, path: []const u8, reader: std.io.AnyReader, size: usize) anyerror!void { + fn streamFile(ptr: *anyopaque, path: []const u8, reader: *std.Io.Reader, size: usize) anyerror!void { const self = @as(*DirExtractWriter, @ptrCast(@alignCast(ptr))); // Create parent directories if needed @@ -510,21 +544,22 @@ pub const DirExtractWriter = struct { // due to internal buffering limitations. We handle this gracefully by reading what's // available rather than treating it as an error. // See: https://github.com/ziglang/zig/issues/[TODO: file issue and add number] - var buffer: [STREAM_BUFFER_SIZE]u8 = undefined; + var file_writer_buffer: [STREAM_BUFFER_SIZE]u8 = undefined; + var file_writer = file.writer(&file_writer_buffer); var total_written: usize = 0; while (total_written < size) { - const bytes_read = reader.read(&buffer) catch |err| { - if (err == error.EndOfStream) break; - return err; + const bytes_read = reader.stream(&file_writer.interface, std.Io.Limit.limited(size - total_written)) catch |err| switch (err) { + error.EndOfStream => break, + error.ReadFailed, error.WriteFailed => return err, }; if (bytes_read == 0) break; - - try file.writeAll(buffer[0..bytes_read]); total_written += bytes_read; } + try file_writer.interface.flush(); + // Verify we got a reasonable amount of data if (total_written == 0 and size > 0) { return error.NoDataExtracted; @@ -538,20 +573,16 @@ pub const DirExtractWriter = struct { /// unbundling and network-based downloading. /// If an InvalidPath error is returned, error_context will contain details about the invalid path. pub fn unbundleStream( - input_reader: anytype, + input_reader: *std.Io.Reader, extract_writer: ExtractWriter, allocator: *std.mem.Allocator, expected_hash: *const [32]u8, error_context: ?*ErrorContext, ) UnbundleError!void { - // Buffered reader for input - var buffered_reader = std.io.bufferedReader(input_reader); - const buffered = buffered_reader.reader(); - // Create decompressing hash reader that chains: input → verify hash → decompress var decompress_reader = streaming_reader.DecompressingHashReader.init( allocator, - buffered.any(), + input_reader, expected_hash.*, allocForZstd, freeForZstd, @@ -563,7 +594,8 @@ pub fn unbundleStream( // Use std.tar to parse the archive; allocate MAX_LENGTH + 1 for null terminator var file_name_buffer: [TAR_PATH_MAX_LENGTH + 1]u8 = undefined; var link_name_buffer: [TAR_PATH_MAX_LENGTH + 1]u8 = undefined; - var tar_iter = std.tar.iterator(decompress_reader.reader(), .{ + + var tar_iter = std.tar.Iterator.init(&decompress_reader.interface, .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer, }); @@ -594,8 +626,9 @@ pub fn unbundleStream( .file => { const tar_file_size = std.math.cast(usize, tar_file.size) orelse return error.FileTooLarge; - // Stream file directly from tar to disk - extract_writer.streamFile(tar_file.name, tar_file.reader().any(), tar_file_size) catch |err| { + var tar_file_reader = TarEntryReader.init(&tar_iter, tar_file.size); + + extract_writer.streamFile(tar_file.name, &tar_file_reader.interface, tar_file_size) catch |err| { switch (err) { error.UnexpectedEndOfStream => return error.UnexpectedEndOfStream, else => return error.FileWriteFailed, @@ -615,11 +648,10 @@ pub fn unbundleStream( } // Ensure all data was read and hash was verified - decompress_reader.verifyComplete() catch |err| switch (err) { - error.HashMismatch => return error.HashMismatch, - error.UnexpectedEndOfStream => return error.UnexpectedEndOfStream, - error.DecompressionFailed => return error.DecompressionFailed, - error.OutOfMemory => return error.OutOfMemory, + decompress_reader.verifyComplete() catch |err| { + switch (err) { + error.HashMismatch => return error.HashMismatch, + } }; } diff --git a/src/bundle/download.zig b/src/bundle/download.zig index 089015625d..a377230a10 100644 --- a/src/bundle/download.zig +++ b/src/bundle/download.zig @@ -160,26 +160,27 @@ pub fn download( } // Start the request with the potentially modified URI - var server_header_buffer: [SERVER_HEADER_BUFFER_SIZE]u8 = undefined; - var request = client.open(.GET, uri, .{ - .server_header_buffer = &server_header_buffer, + var request = client.request(.GET, uri, .{ .redirect_behavior = .unhandled, .extra_headers = extra_headers, }) catch return error.HttpError; defer request.deinit(); - // Send the request - request.send() catch return error.HttpError; - request.finish() catch return error.HttpError; - request.wait() catch return error.HttpError; + // Send just the request head (no body) + request.sendBodiless() catch return error.HttpError; + + // Receive headers into a temporary buffer + var head_buffer: [SERVER_HEADER_BUFFER_SIZE]u8 = undefined; + var response = request.receiveHead(&head_buffer) catch return error.HttpError; // Check response status - if (request.response.status != .ok) { + if (response.head.status != .ok) { return error.HttpError; } - // Get the response reader - const reader = request.reader(); + // Prepare buffered reader for response body + var reader_buffer: [1024]u8 = undefined; + const reader = response.reader(&reader_buffer); // Stream directly to unbundleStream var dir_writer = bundle.DirExtractWriter.init(extract_dir); diff --git a/src/bundle/mod.zig b/src/bundle/mod.zig index 5e49c090de..60d66563ca 100644 --- a/src/bundle/mod.zig +++ b/src/bundle/mod.zig @@ -10,6 +10,7 @@ pub const bundle = @import("bundle.zig"); pub const streaming_writer = @import("streaming_writer.zig"); +pub const streaming_reader = @import("streaming_reader.zig"); // Re-export commonly used functions and types pub const bundleFiles = bundle.bundle; @@ -39,5 +40,4 @@ pub const freeForZstd = bundle.freeForZstd; test { _ = @import("test_bundle.zig"); _ = @import("test_streaming.zig"); - _ = bundle; } diff --git a/src/bundle/streaming_reader.zig b/src/bundle/streaming_reader.zig index 7b1f2d57f6..a60b2dd53f 100644 --- a/src/bundle/streaming_reader.zig +++ b/src/bundle/streaming_reader.zig @@ -14,17 +14,15 @@ pub const DecompressingHashReader = struct { allocator_ptr: *std.mem.Allocator, dctx: *c.ZSTD_DCtx, hasher: std.crypto.hash.Blake3, - input_reader: std.io.AnyReader, + input_reader: *std.Io.Reader, expected_hash: [32]u8, in_buffer: []u8, - out_buffer: []u8, - out_pos: usize, - out_end: usize, - finished: bool, + in_pos: usize, + in_end: usize, hash_verified: bool, + interface: std.Io.Reader, const Self = @This(); - const Reader = std.io.Reader(*Self, Error, read); const Error = error{ DecompressionFailed, UnexpectedEndOfStream, @@ -33,10 +31,10 @@ pub const DecompressingHashReader = struct { pub fn init( allocator_ptr: *std.mem.Allocator, - input_reader: std.io.AnyReader, + input_reader: *std.Io.Reader, expected_hash: [32]u8, - allocForZstd: *const fn (?*anyopaque, usize) callconv(.C) ?*anyopaque, - freeForZstd: *const fn (?*anyopaque, ?*anyopaque) callconv(.C) void, + allocForZstd: *const fn (?*anyopaque, usize) callconv(.c) ?*anyopaque, + freeForZstd: *const fn (?*anyopaque, ?*anyopaque) callconv(.c) void, ) !Self { const custom_mem = c.ZSTD_customMem{ .customAlloc = allocForZstd, @@ -55,122 +53,106 @@ pub const DecompressingHashReader = struct { const out_buffer = try allocator_ptr.alloc(u8, out_buffer_size); errdefer allocator_ptr.free(out_buffer); - return Self{ + var result = Self{ .allocator_ptr = allocator_ptr, .dctx = dctx, .hasher = std.crypto.hash.Blake3.init(.{}), .input_reader = input_reader, .expected_hash = expected_hash, .in_buffer = in_buffer, - .out_buffer = out_buffer, - .out_pos = 0, - .out_end = 0, - .finished = false, + .in_pos = 0, + .in_end = 0, .hash_verified = false, + .interface = undefined, }; + result.interface = .{ + .vtable = &.{ + .stream = stream, + }, + .buffer = out_buffer, + .seek = 0, + .end = 0, + }; + return result; } pub fn deinit(self: *Self) void { _ = c.ZSTD_freeDCtx(self.dctx); self.allocator_ptr.free(self.in_buffer); - self.allocator_ptr.free(self.out_buffer); + self.allocator_ptr.free(self.interface.buffer); } - pub fn reader(self: *Self) Reader { - return .{ .context = self }; - } + fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { + const self: *Self = @alignCast(@fieldParentPtr("interface", r)); - fn read(self: *Self, dest: []u8) Error!usize { - if (dest.len == 0) return 0; + // fill the input buffer as much as possible + var vec: [1][]u8 = .{self.in_buffer[self.in_end..]}; + var reached_end = false; + const bytes_read = self.input_reader.readVec(&vec) catch |err| switch (err) { + error.EndOfStream => blk: { + reached_end = true; + break :blk 0; + }, + error.ReadFailed => return error.ReadFailed, + }; - var total_read: usize = 0; - - while (total_read < dest.len) { - // If we have data in the output buffer, copy it - if (self.out_pos < self.out_end) { - const available = self.out_end - self.out_pos; - const to_copy = @min(available, dest.len - total_read); - @memcpy(dest[total_read..][0..to_copy], self.out_buffer[self.out_pos..][0..to_copy]); - self.out_pos += to_copy; - total_read += to_copy; - - if (total_read == dest.len) { - return total_read; + if (reached_end) { + // verify hash if not already done + if (!self.hash_verified) { + var actual_hash: [32]u8 = undefined; + self.hasher.final(&actual_hash); + if (!std.mem.eql(u8, &actual_hash, &self.expected_hash)) { + return error.ReadFailed; } + self.hash_verified = true; } - - // If finished and no more data in buffer, we're done - if (self.finished) { - break; - } - - // Read more compressed data - const bytes_read = self.input_reader.read(self.in_buffer) catch { - return error.UnexpectedEndOfStream; - }; - - if (bytes_read == 0) { - // End of input stream - verify final hash - if (!self.hash_verified) { - var actual_hash: [32]u8 = undefined; - self.hasher.final(&actual_hash); - if (!std.mem.eql(u8, &actual_hash, &self.expected_hash)) { - return error.HashMismatch; - } - self.hash_verified = true; - } - self.finished = true; - break; - } - - // Update hash with compressed data - self.hasher.update(self.in_buffer[0..bytes_read]); - - // Decompress - var in_buf = c.ZSTD_inBuffer{ .src = self.in_buffer.ptr, .size = bytes_read, .pos = 0 }; - - while (in_buf.pos < in_buf.size) { - var out_buf = c.ZSTD_outBuffer{ .dst = self.out_buffer.ptr, .size = self.out_buffer.len, .pos = 0 }; - - const result = c.ZSTD_decompressStream(self.dctx, &out_buf, &in_buf); - if (c.ZSTD_isError(result) != 0) { - return error.DecompressionFailed; - } - - if (out_buf.pos > 0) { - self.out_pos = 0; - self.out_end = out_buf.pos; - - // Copy what we can to dest - const to_copy = @min(out_buf.pos, dest.len - total_read); - @memcpy(dest[total_read..][0..to_copy], self.out_buffer[0..to_copy]); - self.out_pos = to_copy; - total_read += to_copy; - - if (total_read == dest.len) { - return total_read; - } - } - - // If decompression is complete - if (result == 0) { - break; - } + // actual end is only reached when the input buffer is also empty + if (self.in_end == 0) { + @branchHint(.likely); + return error.EndOfStream; } } - return total_read; + // Update hash with compressed data + self.hasher.update(self.in_buffer[self.in_end..][0..bytes_read]); + self.in_end += bytes_read; + + // Decompress data directly into the output writer + var in_buf = c.ZSTD_inBuffer{ .src = self.in_buffer.ptr, .size = self.in_end, .pos = self.in_pos }; + + const out_data = limit.slice(try w.writableSliceGreedy(1)); + var out_buf = c.ZSTD_outBuffer{ .dst = out_data.ptr, .size = out_data.len, .pos = 0 }; + while (in_buf.pos != in_buf.size) { + const result = c.ZSTD_decompressStream(self.dctx, &out_buf, &in_buf); + if (c.ZSTD_isError(result) != 0) { + // this is still a read failed, as we are not writing to the writer but the internal buffer + return error.ReadFailed; + } + if (out_buf.pos == out_buf.size) { + break; + } + } + if (in_buf.pos < in_buf.size) { + // TODO we could fill the internal reader buffer here + self.in_pos = in_buf.pos; + self.in_end = in_buf.size; + } else { + self.in_pos = 0; + self.in_end = 0; + } + + w.advance(out_buf.pos); + return out_buf.pos; } + /// Verify that the hash matches. This should be called after reading is complete. + /// If there is remaining data, it will be discarded. pub fn verifyComplete(self: *Self) !void { - // Read any remaining data to ensure we process the entire stream - var discard_buffer: [1024]u8 = undefined; - while (true) { - const n = try self.read(&discard_buffer); - if (n == 0) break; - } + _ = self.interface.discardRemaining() catch { + // When the hash does not match, discardRemaining will return a ReadFailed, so we have to ignore it + }; - // The hash should have been verified during reading + // The hash should have been verified during stream if (!self.hash_verified) { return error.HashMismatch; } diff --git a/src/bundle/streaming_writer.zig b/src/bundle/streaming_writer.zig index b3586dcae8..a9a796132c 100644 --- a/src/bundle/streaming_writer.zig +++ b/src/bundle/streaming_writer.zig @@ -9,31 +9,26 @@ const c = @cImport({ @cInclude("zstd.h"); }); +const WriterError = std.io.Writer.Error; + /// A writer that compresses data with zstd and computes a hash incrementally pub const CompressingHashWriter = struct { allocator_ptr: *std.mem.Allocator, ctx: *c.ZSTD_CCtx, hasher: std.crypto.hash.Blake3, - output_writer: std.io.AnyWriter, + output_writer: *std.io.Writer, out_buffer: []u8, - in_buffer: []u8, - in_pos: usize, finished: bool, + interface: std.io.Writer, const Self = @This(); - const Writer = std.io.Writer(*Self, Error, write); - const Error = error{ - CompressionFailed, - WriteFailed, - AlreadyFinished, - } || std.mem.Allocator.Error; pub fn init( allocator_ptr: *std.mem.Allocator, compression_level: c_int, - output_writer: std.io.AnyWriter, - allocForZstd: *const fn (?*anyopaque, usize) callconv(.C) ?*anyopaque, - freeForZstd: *const fn (?*anyopaque, ?*anyopaque) callconv(.C) void, + output_writer: *std.io.Writer, + allocForZstd: *const fn (?*anyopaque, usize) callconv(.c) ?*anyopaque, + freeForZstd: *const fn (?*anyopaque, ?*anyopaque) callconv(.c) void, ) !Self { const custom_mem = c.ZSTD_customMem{ .customAlloc = allocForZstd, @@ -50,65 +45,74 @@ pub const CompressingHashWriter = struct { const out_buffer = try allocator_ptr.alloc(u8, out_buffer_size); errdefer allocator_ptr.free(out_buffer); - const in_buffer_size = c.ZSTD_CStreamInSize(); - const in_buffer = try allocator_ptr.alloc(u8, in_buffer_size); - errdefer allocator_ptr.free(in_buffer); + // Allocate buffer for the Io.Writer interface + const write_buffer_size = c.ZSTD_CStreamInSize(); + const writer_buffer = try allocator_ptr.alloc(u8, write_buffer_size); + errdefer allocator_ptr.free(writer_buffer); - return Self{ + var result = Self{ .allocator_ptr = allocator_ptr, .ctx = ctx, .hasher = std.crypto.hash.Blake3.init(.{}), .output_writer = output_writer, .out_buffer = out_buffer, - .in_buffer = in_buffer, - .in_pos = 0, .finished = false, + .interface = undefined, }; + result.interface = .{ + .vtable = &.{ + .drain = drain, + .flush = flush, + }, + .buffer = writer_buffer, + }; + return result; } pub fn deinit(self: *Self) void { _ = c.ZSTD_freeCCtx(self.ctx); self.allocator_ptr.free(self.out_buffer); - self.allocator_ptr.free(self.in_buffer); + self.allocator_ptr.free(self.interface.buffer); } - pub fn writer(self: *Self) Writer { - return .{ .context = self }; + fn flush(w: *std.io.Writer) WriterError!void { + const self: *Self = @alignCast(@fieldParentPtr("interface", w)); + if (self.finished and w.end != 0) return WriterError.WriteFailed; + _ = self.compressAndHash(w.buffer[0..w.end], false) catch return error.WriteFailed; + w.end = 0; + return; } - fn write(self: *Self, bytes: []const u8) Error!usize { - if (self.finished) return error.AlreadyFinished; + fn drain(w: *std.io.Writer, data: []const []const u8, splat: usize) WriterError!usize { + const self: *Self = @alignCast(@fieldParentPtr("interface", w)); + if (self.finished) return WriterError.WriteFailed; + _ = self.compressAndHash(w.buffer[0..w.end], false) catch return error.WriteFailed; + w.end = 0; + if (data.len == 0) return 0; var written: usize = 0; - while (written < bytes.len) { - // Fill input buffer - const space_available = self.in_buffer.len - self.in_pos; - const to_copy = @min(space_available, bytes.len - written); - @memcpy(self.in_buffer[self.in_pos..][0..to_copy], bytes[written..][0..to_copy]); - self.in_pos += to_copy; - written += to_copy; + for (data[0 .. data.len - 1]) |bytes| { + const len = self.compressAndHash(bytes, false) catch return error.WriteFailed; + written += len; + } - // If buffer is full, compress it - if (self.in_pos == self.in_buffer.len) { - try self.compressBuffer(false); - } + const pattern = data[data.len - 1]; + for (0..splat) |_| { + const len = self.compressAndHash(pattern, false) catch return error.WriteFailed; + written += len; } return written; } - fn compressBuffer(self: *Self, end_stream: bool) Error!void { - if (self.in_pos == 0 and !end_stream) return; - - var in_buf = c.ZSTD_inBuffer{ .src = self.in_buffer.ptr, .size = self.in_pos, .pos = 0 }; - + fn compressAndHash(self: *Self, data: []const u8, end_stream: bool) WriterError!usize { + var in_buf = c.ZSTD_inBuffer{ .src = data.ptr, .size = data.len, .pos = 0 }; const mode: c_uint = if (end_stream) c.ZSTD_e_end else c.ZSTD_e_continue; - while (in_buf.pos < in_buf.size or end_stream) { var out_buf = c.ZSTD_outBuffer{ .dst = self.out_buffer.ptr, .size = self.out_buffer.len, .pos = 0 }; const remaining = c.ZSTD_compressStream2(self.ctx, &out_buf, &in_buf, mode); if (c.ZSTD_isError(remaining) != 0) { - return error.CompressionFailed; + return WriterError.WriteFailed; } if (out_buf.pos > 0) { @@ -119,13 +123,13 @@ pub const CompressingHashWriter = struct { if (end_stream and remaining == 0) break; } - - self.in_pos = 0; + return in_buf.pos; } - pub fn finish(self: *Self) Error!void { + pub fn finish(self: *Self) WriterError!void { if (self.finished) return; - try self.compressBuffer(true); + _ = self.compressAndHash(self.interface.buffer[0..self.interface.end], true) catch return error.WriteFailed; + self.interface.end = 0; self.finished = true; } diff --git a/src/bundle/test_bundle.zig b/src/bundle/test_bundle.zig index 2a32e5149a..9a35793b86 100644 --- a/src/bundle/test_bundle.zig +++ b/src/bundle/test_bundle.zig @@ -12,7 +12,9 @@ const bundle = @import("bundle.zig"); const download = @import("download.zig"); const streaming_writer = @import("streaming_writer.zig"); const test_util = @import("test_util.zig"); +const unbundle_mod = @import("unbundle"); const DirExtractWriter = bundle.DirExtractWriter; +const BufferExtractWriter = unbundle_mod.BufferExtractWriter; const FilePathIterator = test_util.FilePathIterator; // Use fast compression for tests @@ -21,7 +23,6 @@ const TEST_COMPRESSION_LEVEL: c_int = 2; test "path validation for unbundle prevents security issues" { const testing = std.testing; - // Test cases for pathHasUnbundleErr (used in unbundle) - only security checks const test_cases = [_]struct { path: []const u8, should_fail: bool, @@ -64,7 +65,7 @@ test "path validation for unbundle prevents security issues" { try testing.expect(!is_valid); } else { if (validation_result) |err| { - std.debug.print("Unexpected validation failure for '{s}': {}\n", .{ tc.path, err.reason }); + std.debug.print("Unexpected validation failure for '{s}': {any}\n", .{ tc.path, err.reason }); } try testing.expect(is_valid); } @@ -116,7 +117,8 @@ test "path validation for bundle prevents Windows issues" { .{ .path = "foo/bar.txt", .should_fail = false, .description = "Valid path" }, .{ .path = "src/main.zig", .should_fail = false, .description = "Valid source path" }, .{ .path = "a-b_c.123", .should_fail = false, .description = "Valid filename with special chars" }, - .{ .path = "deeply/nested/folder/structure/file.ext", .should_fail = false, .description = "Valid nested path" }, + .{ .path = "test-folder/tests.zig", .should_fail = false, .description = "Path with dash" }, + .{ .path = "nested/folder/structure.txt", .should_fail = false, .description = "Nested path" }, }; for (test_cases) |tc| { @@ -127,7 +129,7 @@ test "path validation for bundle prevents Windows issues" { try testing.expect(!is_valid); } else { if (validation_result) |err| { - std.debug.print("Unexpected validation failure for '{s}': {}\n", .{ tc.path, err.reason }); + std.debug.print("Unexpected validation failure for '{s}': {any}\n", .{ tc.path, err.reason }); } try testing.expect(is_valid); } @@ -208,14 +210,14 @@ test "bundle validates paths correctly" { try file.writeAll("Test content"); } { - var bundle_data = std.ArrayList(u8).init(allocator); - defer bundle_data.deinit(); + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); const paths = [_][]const u8{"CON.txt"}; var iter = FilePathIterator{ .paths = &paths }; var error_ctx: bundle.ErrorContext = undefined; - const result = bundle.bundle(&iter, TEST_COMPRESSION_LEVEL, &allocator, bundle_data.writer(), tmp.dir, null, &error_ctx); + const result = bundle.bundle(&iter, TEST_COMPRESSION_LEVEL, &allocator, &bundle_writer.writer, tmp.dir, null, &error_ctx); try testing.expectError(error.InvalidPath, result); try testing.expectEqual(bundle.PathValidationReason.windows_reserved_name, error_ctx.reason); @@ -228,17 +230,19 @@ test "bundle validates paths correctly" { try file.writeAll("Normal content"); } { - var bundle_data = std.ArrayList(u8).init(allocator); - defer bundle_data.deinit(); + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); const paths = [_][]const u8{"normal.txt"}; var iter = FilePathIterator{ .paths = &paths }; - const filename = try bundle.bundle(&iter, TEST_COMPRESSION_LEVEL, &allocator, bundle_data.writer(), tmp.dir, null, null); + const filename = try bundle.bundle(&iter, TEST_COMPRESSION_LEVEL, &allocator, &bundle_writer.writer, tmp.dir, null, null); defer allocator.free(filename); // Should succeed - try testing.expect(bundle_data.items.len > 0); + var list = bundle_writer.toArrayList(); + defer list.deinit(allocator); + try testing.expect(list.items.len > 0); } } @@ -247,10 +251,10 @@ test "path validation prevents directory traversal" { const allocator = testing.allocator; // Create a malicious tar with directory traversal attempt - var malicious_tar = std.ArrayList(u8).init(allocator); + var malicious_tar: std.Io.Writer.Allocating = .init(allocator); defer malicious_tar.deinit(); - var tar_writer = std.tar.writer(malicious_tar.writer()); + var tar_writer = std.tar.Writer{ .underlying_writer = &malicious_tar.writer }; // Try to write a file with ".." in path const Options = @TypeOf(tar_writer).Options; @@ -260,24 +264,27 @@ test "path validation prevents directory traversal" { }; try tar_writer.writeFileBytes("../../../etc/passwd", "malicious content", options); - try tar_writer.finish(); + try tar_writer.finishPedantically(); // Compress it - var compressed = std.ArrayList(u8).init(allocator); - defer compressed.deinit(); + var compressed_writer: std.Io.Writer.Allocating = .init(allocator); + defer compressed_writer.deinit(); var allocator_copy = allocator; var writer = try streaming_writer.CompressingHashWriter.init( &allocator_copy, 3, - compressed.writer().any(), + &compressed_writer.writer, bundle.allocForZstd, bundle.freeForZstd, ); defer writer.deinit(); - try writer.writer().writeAll(malicious_tar.items); + const malicious_tar_data = try malicious_tar.toOwnedSlice(); + defer allocator.free(malicious_tar_data); + try writer.interface.writeAll(malicious_tar_data); try writer.finish(); + try writer.interface.flush(); const hash = writer.getHash(); @@ -285,11 +292,14 @@ test "path validation prevents directory traversal" { var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); - var stream = std.io.fixedBufferStream(compressed.items); + var compressed_list = compressed_writer.toArrayList(); + defer compressed_list.deinit(allocator); + + var stream_reader = std.Io.Reader.fixed(compressed_list.items); var allocator_copy2 = allocator; var dir_writer = DirExtractWriter.init(tmp.dir); const result = bundle.unbundleStream( - stream.reader(), + &stream_reader, dir_writer.extractWriter(), &allocator_copy2, &hash, @@ -320,24 +330,27 @@ test "empty directories are preserved" { } // Bundle with explicit directory entries - var bundle_data = std.ArrayList(u8).init(allocator); - defer bundle_data.deinit(); + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); // Note: Current implementation doesn't explicitly handle empty directories // This test documents current behavior - empty dirs are NOT preserved const file_paths = [_][]const u8{"readme.txt"}; var file_iter = FilePathIterator{ .paths = &file_paths }; - const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, bundle_data.writer(), src_dir, null, null); + const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, &bundle_writer.writer, src_dir, null, null); defer allocator.free(filename); // Extract var dst_tmp = testing.tmpDir(.{}); defer dst_tmp.cleanup(); - var stream = std.io.fixedBufferStream(bundle_data.items); + var bundle_list = bundle_writer.toArrayList(); + defer bundle_list.deinit(allocator); + + var stream_reader = std.Io.Reader.fixed(bundle_list.items); var allocator_copy = allocator; - try bundle.unbundle(stream.reader(), dst_tmp.dir, &allocator_copy, filename, null); + try bundle.unbundle(&stream_reader, dst_tmp.dir, &allocator_copy, filename, null); // Verify file exists _ = try dst_tmp.dir.statFile("readme.txt"); @@ -399,10 +412,10 @@ test "bundle and unbundle roundtrip" { var file_iter = FilePathIterator{ .paths = &file_paths }; // Bundle to memory - var bundle_data = std.ArrayList(u8).init(allocator); - defer bundle_data.deinit(); + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); - const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, bundle_data.writer(), src_dir, null, null); + const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, &bundle_writer.writer, src_dir, null, null); defer allocator.free(filename); // Create destination temp directory @@ -411,8 +424,11 @@ test "bundle and unbundle roundtrip" { const dst_dir = dst_tmp.dir; // Unbundle from memory - var stream = std.io.fixedBufferStream(bundle_data.items); - try bundle.unbundle(stream.reader(), dst_dir, &allocator, filename, null); + var bundle_list = bundle_writer.toArrayList(); + defer bundle_list.deinit(allocator); + + var stream_reader = std.Io.Reader.fixed(bundle_list.items); + try bundle.unbundle(&stream_reader, dst_dir, &allocator, filename, null); // Verify all files exist with correct content const file1_content = try dst_dir.readFileAlloc(allocator, "file1.txt", 1024); @@ -482,7 +498,10 @@ test "bundle and unbundle over socket stream" { }; var file_iter = FilePathIterator{ .paths = &file_paths }; - const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, bundle_file.writer(), src_dir, null, null); + var bundle_writer_buffer: [4096]u8 = undefined; + var bundle_writer = bundle_file.writer(&bundle_writer_buffer); + const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, &bundle_writer.interface, src_dir, null, null); + try bundle_writer.interface.flush(); defer allocator.free(filename); // Create socket in temp directory @@ -550,11 +569,14 @@ test "bundle and unbundle over socket stream" { const dst_dir = dst_tmp.dir; // Connect to socket and unbundle - const stream = try std.net.connectUnixSocket(socket_path); + var stream = try std.net.connectUnixSocket(socket_path); defer stream.close(); - // Unbundle from socket stream - try bundle.unbundle(stream.reader(), dst_dir, &allocator, filename, null); + // Unbundle from socket stream using new reader interface + var stream_buffer: [1024]u8 = undefined; + var buffered_reader = stream.reader(&stream_buffer); + const socket_reader = &buffered_reader.file_reader.interface; + try bundle.unbundle(socket_reader, dst_dir, &allocator, filename, null); // Wait for server to finish server_ctx.done.wait(); @@ -578,10 +600,10 @@ test "std.tar.writer creates valid tar" { const allocator = testing.allocator; // Create a tar in memory - var tar_buffer = std.ArrayList(u8).init(allocator); - defer tar_buffer.deinit(); + var allocating_writer: std.Io.Writer.Allocating = .init(allocator); + defer allocating_writer.deinit(); - var tar_writer = std.tar.writer(tar_buffer.writer()); + var tar_writer = std.tar.Writer{ .underlying_writer = &allocating_writer.writer }; // Write a simple file const content = "Hello tar world!"; @@ -591,13 +613,16 @@ test "std.tar.writer creates valid tar" { .mtime = 0, }); - try tar_writer.finish(); + try tar_writer.finishPedantically(); // Now try to read it back - var stream = std.io.fixedBufferStream(tar_buffer.items); + const tar_bytes = try allocating_writer.toOwnedSlice(); + defer allocator.free(tar_bytes); + + var tar_reader = std.Io.Reader.fixed(tar_bytes); var file_name_buffer: [256]u8 = undefined; var link_name_buffer: [256]u8 = undefined; - var tar_iter = std.tar.iterator(stream.reader(), .{ + var tar_iter = std.tar.Iterator.init(&tar_reader, .{ .file_name_buffer = &file_name_buffer, .link_name_buffer = &link_name_buffer, }); @@ -610,7 +635,7 @@ test "std.tar.writer creates valid tar" { // Read content const reader = tar_iter.reader; var buf: [1024]u8 = undefined; - const bytes_read = try reader.read(buf[0..content.len]); + const bytes_read = try reader.readSliceShort(buf[0..content.len]); try testing.expectEqualStrings(content, buf[0..bytes_read]); } @@ -631,12 +656,12 @@ test "minimal bundle unbundle" { } // Bundle to memory - var bundle_data = std.ArrayList(u8).init(allocator); - defer bundle_data.deinit(); + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); const file_paths = [_][]const u8{"test.txt"}; var file_iter = FilePathIterator{ .paths = &file_paths }; - const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, bundle_data.writer(), src_dir, null, null); + const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, &bundle_writer.writer, src_dir, null, null); defer allocator.free(filename); // Create destination temp directory @@ -645,8 +670,11 @@ test "minimal bundle unbundle" { const dst_dir = dst_tmp.dir; // Unbundle from memory - var stream = std.io.fixedBufferStream(bundle_data.items); - try bundle.unbundle(stream.reader(), dst_dir, &allocator, filename, null); + var bundle_list = bundle_writer.toArrayList(); + defer bundle_list.deinit(allocator); + + var stream_reader = std.Io.Reader.fixed(bundle_list.items); + try bundle.unbundle(&stream_reader, dst_dir, &allocator, filename, null); // Read and verify content const content = try dst_dir.readFileAlloc(allocator, "test.txt", 1024); @@ -680,8 +708,8 @@ test "bundle with path prefix stripping" { } // Bundle with path prefix - var bundle_data = std.ArrayList(u8).init(allocator); - defer bundle_data.deinit(); + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); // File paths include the full prefix const file_paths = [_][]const u8{ @@ -692,7 +720,7 @@ test "bundle with path prefix stripping" { var file_iter = FilePathIterator{ .paths = &file_paths }; // Bundle with prefix "foo/bar/src/" - const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, bundle_data.writer(), src_dir, "foo/bar/src/", null); + const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, &bundle_writer.writer, src_dir, "foo/bar/src/", null); defer allocator.free(filename); // Create destination temp directory @@ -701,8 +729,11 @@ test "bundle with path prefix stripping" { const dst_dir = dst_tmp.dir; // Unbundle - var stream = std.io.fixedBufferStream(bundle_data.items); - try bundle.unbundle(stream.reader(), dst_dir, &allocator, filename, null); + var bundle_list = bundle_writer.toArrayList(); + defer bundle_list.deinit(allocator); + + var stream_reader = std.Io.Reader.fixed(bundle_list.items); + try bundle.unbundle(&stream_reader, dst_dir, &allocator, filename, null); // Verify files exist WITHOUT the prefix const main_content = try dst_dir.readFileAlloc(allocator, "main.txt", 1024); @@ -730,12 +761,12 @@ test "blake3 hash verification success" { } // Bundle the file - var bundle_data = std.ArrayList(u8).init(allocator); - defer bundle_data.deinit(); + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); const file_paths = [_][]const u8{"test.txt"}; var file_iter = FilePathIterator{ .paths = &file_paths }; - const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, bundle_data.writer(), src_dir, null, null); + const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, &bundle_writer.writer, src_dir, null, null); defer allocator.free(filename); // Verify filename ends with .tar.zst @@ -747,8 +778,11 @@ test "blake3 hash verification success" { const dst_dir = dst_tmp.dir; // Unbundle with correct filename - should succeed - var stream = std.io.fixedBufferStream(bundle_data.items); - try bundle.unbundle(stream.reader(), dst_dir, &allocator, filename, null); + var bundle_list = bundle_writer.toArrayList(); + defer bundle_list.deinit(allocator); + + var stream_reader = std.Io.Reader.fixed(bundle_list.items); + try bundle.unbundle(&stream_reader, dst_dir, &allocator, filename, null); // Verify content const content = try dst_dir.readFileAlloc(allocator, "test.txt", 1024); @@ -772,12 +806,12 @@ test "blake3 hash verification failure" { } // Bundle the file - var bundle_data = std.ArrayList(u8).init(allocator); - defer bundle_data.deinit(); + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); const file_paths = [_][]const u8{"test.txt"}; var file_iter = FilePathIterator{ .paths = &file_paths }; - const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, bundle_data.writer(), src_dir, null, null); + const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, &bundle_writer.writer, src_dir, null, null); defer allocator.free(filename); // Create destination directory @@ -787,8 +821,11 @@ test "blake3 hash verification failure" { // Try to unbundle with wrong filename - should fail const wrong_filename = "1234567890abcdef.tar.zst"; - var stream = std.io.fixedBufferStream(bundle_data.items); - const result = bundle.unbundle(stream.reader(), dst_dir, &allocator, wrong_filename, null); + var bundle_list = bundle_writer.toArrayList(); + defer bundle_list.deinit(allocator); + + var stream_reader = std.Io.Reader.fixed(bundle_list.items); + const result = bundle.unbundle(&stream_reader, dst_dir, &allocator, wrong_filename, null); try testing.expectError(error.InvalidFilename, result); } @@ -803,8 +840,8 @@ test "unbundle with existing directory error" { const tmp_dir = tmp.dir; // Create a simple tar archive - var output_buffer = std.ArrayList(u8).init(allocator); - defer output_buffer.deinit(); + var output_writer: std.Io.Writer.Allocating = .init(allocator); + defer output_writer.deinit(); const files = [_][]const u8{"test.txt"}; var iter = FilePathIterator{ .paths = &files }; @@ -817,14 +854,16 @@ test "unbundle with existing directory error" { } // Bundle the file - const filename = try bundle.bundle(&iter, TEST_COMPRESSION_LEVEL, &allocator, output_buffer.writer(), tmp_dir, null, null); + const filename = try bundle.bundle(&iter, TEST_COMPRESSION_LEVEL, &allocator, &output_writer.writer, tmp_dir, null, null); defer allocator.free(filename); // Write the bundled data to a file + var output_list = output_writer.toArrayList(); + defer output_list.deinit(allocator); { const bundle_file = try tmp_dir.createFile(filename, .{}); defer bundle_file.close(); - try bundle_file.writeAll(output_buffer.items); + try bundle_file.writeAll(output_list.items); } // Extract the base name without extension for directory @@ -837,8 +876,11 @@ test "unbundle with existing directory error" { const bundle_file = try tmp_dir.openFile(filename, .{}); defer bundle_file.close(); + var bundle_reader_buffer: [4096]u8 = undefined; + var bundle_reader = bundle_file.reader(&bundle_reader_buffer); + // This should succeed but the CLI would error on existing directory - try bundle.unbundle(bundle_file.reader(), tmp_dir, &allocator, filename, null); + try bundle.unbundle(&bundle_reader.interface, tmp_dir, &allocator, filename, null); } test "unbundle multiple archives" { @@ -851,7 +893,7 @@ test "unbundle multiple archives" { const tmp_dir = tmp.dir; // Create two different archives - var filenames = std.ArrayList([]const u8).init(allocator); + var filenames = std.array_list.Managed([]const u8).init(allocator); defer { for (filenames.items) |fname| { allocator.free(fname); @@ -861,8 +903,8 @@ test "unbundle multiple archives" { // First archive { - var output_buffer = std.ArrayList(u8).init(allocator); - defer output_buffer.deinit(); + var output_writer: std.Io.Writer.Allocating = .init(allocator); + defer output_writer.deinit(); const files = [_][]const u8{"file1.txt"}; var iter = FilePathIterator{ .paths = &files }; @@ -873,18 +915,20 @@ test "unbundle multiple archives" { try file.writeAll("content 1"); } - const filename = try bundle.bundle(&iter, TEST_COMPRESSION_LEVEL, &allocator, output_buffer.writer(), tmp_dir, null, null); + const filename = try bundle.bundle(&iter, TEST_COMPRESSION_LEVEL, &allocator, &output_writer.writer, tmp_dir, null, null); try filenames.append(filename); + var output_list = output_writer.toArrayList(); + defer output_list.deinit(allocator); const bundle_file = try tmp_dir.createFile(filename, .{}); defer bundle_file.close(); - try bundle_file.writeAll(output_buffer.items); + try bundle_file.writeAll(output_list.items); } // Second archive { - var output_buffer = std.ArrayList(u8).init(allocator); - defer output_buffer.deinit(); + var output_writer: std.Io.Writer.Allocating = .init(allocator); + defer output_writer.deinit(); const files = [_][]const u8{"file2.txt"}; var iter = FilePathIterator{ .paths = &files }; @@ -895,12 +939,14 @@ test "unbundle multiple archives" { try file.writeAll("content 2"); } - const filename = try bundle.bundle(&iter, TEST_COMPRESSION_LEVEL, &allocator, output_buffer.writer(), tmp_dir, null, null); + const filename = try bundle.bundle(&iter, TEST_COMPRESSION_LEVEL, &allocator, &output_writer.writer, tmp_dir, null, null); try filenames.append(filename); + var output_list = output_writer.toArrayList(); + defer output_list.deinit(allocator); const bundle_file = try tmp_dir.createFile(filename, .{}); defer bundle_file.close(); - try bundle_file.writeAll(output_buffer.items); + try bundle_file.writeAll(output_list.items); } // Unbundle both archives @@ -911,7 +957,9 @@ test "unbundle multiple archives" { const dir_name = fname[0 .. fname.len - 8]; // Remove .tar.zst const extract_dir = try tmp_dir.makeOpenPath(dir_name, .{}); - try bundle.unbundle(bundle_file.reader(), extract_dir, &allocator, fname, null); + var reader_buffer: [4096]u8 = undefined; + var bundle_reader = bundle_file.reader(&reader_buffer); + try bundle.unbundle(&bundle_reader.interface, extract_dir, &allocator, fname, null); } // Verify extraction @@ -947,19 +995,22 @@ test "blake3 hash detects corruption" { } // Bundle the file - var bundle_data = std.ArrayList(u8).init(allocator); - defer bundle_data.deinit(); + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); const file_paths = [_][]const u8{"test.txt"}; var file_iter = FilePathIterator{ .paths = &file_paths }; - const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, bundle_data.writer(), src_dir, null, null); + const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, &bundle_writer.writer, src_dir, null, null); defer allocator.free(filename); // Corrupt the data by flipping a bit // Since the bundle is compressed, corrupting any bit should be detected - if (bundle_data.items.len > 10) { + var bundle_list = bundle_writer.toArrayList(); + defer bundle_list.deinit(allocator); + + if (bundle_list.items.len > 10) { // Corrupt a bit near the end to avoid breaking the zstd header - bundle_data.items[bundle_data.items.len - 5] ^= 0x01; + bundle_list.items[bundle_list.items.len - 5] ^= 0x01; } // Create destination directory @@ -968,8 +1019,8 @@ test "blake3 hash detects corruption" { const dst_dir = dst_tmp.dir; // Try to unbundle corrupted data - should fail with HashMismatch or DecompressionFailed - var stream = std.io.fixedBufferStream(bundle_data.items); - const result = bundle.unbundle(stream.reader(), dst_dir, &allocator, filename, null); + var stream_reader = std.Io.Reader.fixed(bundle_list.items); + const result = bundle.unbundle(&stream_reader, dst_dir, &allocator, filename, null); // Corruption can cause either hash mismatch (if decompression succeeds but data is wrong) // or decompression failure (if the compressed stream structure is corrupted) @@ -1015,24 +1066,26 @@ test "double roundtrip bundle -> unbundle -> bundle -> unbundle" { } // First bundle - var first_bundle = std.ArrayList(u8).init(allocator); - defer first_bundle.deinit(); + var first_bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer first_bundle_writer.deinit(); - var paths1 = std.ArrayList([]const u8).init(allocator); - defer paths1.deinit(); + var paths1 = std.ArrayList([]const u8).empty; + defer paths1.deinit(allocator); for (test_files) |test_file| { - try paths1.append(test_file.path); + try paths1.append(allocator, test_file.path); } var iter1 = FilePathIterator{ .paths = paths1.items }; - const filename1 = try bundle.bundle(&iter1, TEST_COMPRESSION_LEVEL, &allocator, first_bundle.writer(), initial_dir, null, null); + const filename1 = try bundle.bundle(&iter1, TEST_COMPRESSION_LEVEL, &allocator, &first_bundle_writer.writer, initial_dir, null, null); defer allocator.free(filename1); // Write first bundle to file + var first_bundle_list = first_bundle_writer.toArrayList(); + defer first_bundle_list.deinit(allocator); { const bundle_file = try initial_dir.createFile(filename1, .{}); defer bundle_file.close(); - try bundle_file.writeAll(first_bundle.items); + try bundle_file.writeAll(first_bundle_list.items); } // First unbundle @@ -1046,32 +1099,36 @@ test "double roundtrip bundle -> unbundle -> bundle -> unbundle" { const extract_dir = try unbundle1_dir.makeOpenPath("extracted1", .{}); - try bundle.unbundle(bundle_file.reader(), extract_dir, &allocator, filename1, null); + var reader_buffer: [4096]u8 = undefined; + var bundle_reader = bundle_file.reader(&reader_buffer); + try bundle.unbundle(&bundle_reader.interface, extract_dir, &allocator, filename1, null); } // Second bundle (from first extraction) - var second_bundle = std.ArrayList(u8).init(allocator); - defer second_bundle.deinit(); + var second_bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer second_bundle_writer.deinit(); - var paths2 = std.ArrayList([]const u8).init(allocator); - defer paths2.deinit(); + var paths2 = std.ArrayList([]const u8).empty; + defer paths2.deinit(allocator); for (test_files) |test_file| { - try paths2.append(test_file.path); + try paths2.append(allocator, test_file.path); } var iter2 = FilePathIterator{ .paths = paths2.items }; const extracted1_dir = try unbundle1_dir.openDir("extracted1", .{}); - const filename2 = try bundle.bundle(&iter2, TEST_COMPRESSION_LEVEL, &allocator, second_bundle.writer(), extracted1_dir, null, null); + const filename2 = try bundle.bundle(&iter2, TEST_COMPRESSION_LEVEL, &allocator, &second_bundle_writer.writer, extracted1_dir, null, null); defer allocator.free(filename2); // Filenames should be identical (same content = same hash) try testing.expectEqualStrings(filename1, filename2); // Write second bundle to file + var second_bundle_list = second_bundle_writer.toArrayList(); + defer second_bundle_list.deinit(allocator); { const bundle_file = try unbundle1_dir.createFile(filename2, .{}); defer bundle_file.close(); - try bundle_file.writeAll(second_bundle.items); + try bundle_file.writeAll(second_bundle_list.items); } // Second unbundle @@ -1085,7 +1142,9 @@ test "double roundtrip bundle -> unbundle -> bundle -> unbundle" { const extract_dir = try unbundle2_dir.makeOpenPath("extracted2", .{}); - try bundle.unbundle(bundle_file.reader(), extract_dir, &allocator, filename2, null); + var reader_buffer: [4096]u8 = undefined; + var bundle_reader = bundle_file.reader(&reader_buffer); + try bundle.unbundle(&bundle_reader.interface, extract_dir, &allocator, filename2, null); } // Verify all files match original content @@ -1097,7 +1156,7 @@ test "double roundtrip bundle -> unbundle -> bundle -> unbundle" { } // Bundle sizes should be identical - try testing.expectEqual(first_bundle.items.len, second_bundle.items.len); + try testing.expectEqual(first_bundle_list.items.len, second_bundle_list.items.len); } test "CLI unbundle with no args defaults to all .tar.zst files" { @@ -1110,7 +1169,7 @@ test "CLI unbundle with no args defaults to all .tar.zst files" { const tmp_dir = tmp.dir; // Create multiple archives - var archive_names = std.ArrayList([]const u8).init(allocator); + var archive_names = std.array_list.Managed([]const u8).init(allocator); defer { for (archive_names.items) |name| { allocator.free(name); @@ -1120,8 +1179,8 @@ test "CLI unbundle with no args defaults to all .tar.zst files" { // Create 3 different archives for ([_][]const u8{ "file1.txt", "file2.txt", "file3.txt" }) |filename| { - var output_buffer = std.ArrayList(u8).init(allocator); - defer output_buffer.deinit(); + var output_writer: std.Io.Writer.Allocating = .init(allocator); + defer output_writer.deinit(); const files = [_][]const u8{filename}; var iter = FilePathIterator{ .paths = &files }; @@ -1130,16 +1189,21 @@ test "CLI unbundle with no args defaults to all .tar.zst files" { { const file = try tmp_dir.createFile(filename, .{}); defer file.close(); - try file.writer().print("Content of {s}", .{filename}); + var writer_buffer: [256]u8 = undefined; + var file_writer = file.writer(&writer_buffer); + try file_writer.interface.print("Content of {s}", .{filename}); + try file_writer.interface.flush(); } - const archive_name = try bundle.bundle(&iter, TEST_COMPRESSION_LEVEL, &allocator, output_buffer.writer(), tmp_dir, null, null); + const archive_name = try bundle.bundle(&iter, TEST_COMPRESSION_LEVEL, &allocator, &output_writer.writer, tmp_dir, null, null); try archive_names.append(archive_name); // Write archive to disk + var output_list = output_writer.toArrayList(); + defer output_list.deinit(allocator); const archive_file = try tmp_dir.createFile(archive_name, .{}); defer archive_file.close(); - try archive_file.writeAll(output_buffer.items); + try archive_file.writeAll(output_list.items); } // Verify all archives exist @@ -1154,13 +1218,13 @@ test "CLI unbundle with no args defaults to all .tar.zst files" { var cwd = try tmp_dir.openDir(".", .{ .iterate = true }); defer cwd.close(); - var found_archives = std.ArrayList([]const u8).init(allocator); - defer found_archives.deinit(); + var found_archives = std.ArrayList([]const u8).empty; + defer found_archives.deinit(allocator); var iter = cwd.iterate(); while (try iter.next()) |entry| { if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".tar.zst")) { - try found_archives.append(entry.name); + try found_archives.append(allocator, entry.name); } } @@ -1180,7 +1244,7 @@ test "download URL validation" { const url = "https://example.com/path/to/4ZGqXJtqH5n9wMmQ7nPQTU8zgHBNfZ3kcVnNcL3hKqXf.tar.zst"; _ = download.validateUrl(url) catch |err| { try testing.expect(false); // Should not error - std.debug.print("Unexpected error: {}\n", .{err}); + std.debug.print("Unexpected error: {any}\n", .{err}); }; } @@ -1189,7 +1253,7 @@ test "download URL validation" { const url = "http://127.0.0.1:8000/4ZGqXJtqH5n9wMmQ7nPQTU8zgHBNfZ3kcVnNcL3hKqXf.tar.zst"; _ = download.validateUrl(url) catch |err| { try testing.expect(false); // Should not error - std.debug.print("Unexpected error: {}\n", .{err}); + std.debug.print("Unexpected error: {any}\n", .{err}); }; } @@ -1198,7 +1262,7 @@ test "download URL validation" { const url = "http://[::1]:8000/4ZGqXJtqH5n9wMmQ7nPQTU8zgHBNfZ3kcVnNcL3hKqXf.tar.zst"; _ = download.validateUrl(url) catch |err| { try testing.expect(false); // Should not error - std.debug.print("Unexpected error: {}\n", .{err}); + std.debug.print("Unexpected error: {any}\n", .{err}); }; } @@ -1207,7 +1271,7 @@ test "download URL validation" { const url = "http://[::1]/4ZGqXJtqH5n9wMmQ7nPQTU8zgHBNfZ3kcVnNcL3hKqXf.tar.zst"; _ = download.validateUrl(url) catch |err| { try testing.expect(false); // Should not error - std.debug.print("Unexpected error: {}\n", .{err}); + std.debug.print("Unexpected error: {any}\n", .{err}); }; } @@ -1250,13 +1314,13 @@ test "download URL validation" { // In-memory file system for testing const MemoryFileSystem = struct { allocator: std.mem.Allocator, - files: std.StringHashMap(std.ArrayList(u8)), + files: std.StringHashMap(std.array_list.Managed(u8)), directories: std.StringHashMap(void), pub fn init(allocator: std.mem.Allocator) MemoryFileSystem { return .{ .allocator = allocator, - .files = std.StringHashMap(std.ArrayList(u8)).init(allocator), + .files = std.StringHashMap(std.array_list.Managed(u8)).init(allocator), .directories = std.StringHashMap(void).init(allocator), }; } @@ -1291,7 +1355,7 @@ const MemoryFileSystem = struct { } } - fn streamFile(ptr: *anyopaque, path: []const u8, reader: std.io.AnyReader, size: usize) anyerror!void { + fn streamFile(ptr: *anyopaque, path: []const u8, reader: *std.Io.Reader, size: usize) anyerror!void { const self = @as(*MemoryFileSystem, @ptrCast(@alignCast(ptr))); // Create parent directories if needed @@ -1302,7 +1366,7 @@ const MemoryFileSystem = struct { } // Create new file data - var file_data = std.ArrayList(u8).init(self.allocator); + var file_data = std.array_list.Managed(u8).init(self.allocator); // Stream from reader var buffer: [bundle.STREAM_BUFFER_SIZE]u8 = undefined; @@ -1362,8 +1426,8 @@ test "download from local server" { } // Bundle the files - var bundle_data = std.ArrayList(u8).init(allocator); - defer bundle_data.deinit(); + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); const file_paths = [_][]const u8{ "README.md", @@ -1372,15 +1436,18 @@ test "download from local server" { }; var file_iter = FilePathIterator{ .paths = &file_paths }; - const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, bundle_data.writer(), tmp.dir, null, null); + const filename = try bundle.bundle(&file_iter, TEST_COMPRESSION_LEVEL, &allocator, &bundle_writer.writer, tmp.dir, null, null); defer allocator.free(filename); // Extract hash from filename const base58_hash = filename[0 .. filename.len - 8]; // Remove .tar.zst + var bundle_list = bundle_writer.toArrayList(); + defer bundle_list.deinit(allocator); + // Create HTTP server on port 0 (let OS assign available port) const loopback = try std.net.Address.parseIp("127.0.0.1", 0); - var server = try loopback.listen(.{ .reuse_port = true }); + var server = try loopback.listen(.{ .reuse_address = true }); defer server.deinit(); // Get the actual port assigned by the OS @@ -1412,7 +1479,13 @@ test "download from local server" { // Read HTTP request var request_buf: [4096]u8 = undefined; - const bytes_read = try connection.stream.read(&request_buf); + var recv_buffer: [512]u8 = undefined; + var conn_reader = connection.stream.reader(&recv_buffer); + var slices = [_][]u8{request_buf[0..]}; + const bytes_read = std.Io.Reader.readVec(conn_reader.interface(), &slices) catch |err| switch (err) { + error.EndOfStream => 0, + error.ReadFailed => return conn_reader.getError() orelse error.Unexpected, + }; // Parse request line to get the path const request = request_buf[0..bytes_read]; @@ -1436,7 +1509,7 @@ test "download from local server" { var server_ctx = ServerContext{ .server = &server, - .bundle_data = bundle_data.items, + .bundle_data = bundle_list.items, .allocator = allocator, }; @@ -1497,3 +1570,169 @@ test "download from local server" { // If we got here, src directory exists } } + +// Test unbundleStream with BufferExtractWriter - simulates WASM usage +// This tests the full pipeline: zstd decompression -> tar extraction -> memory buffer +test "unbundleStream with BufferExtractWriter (WASM simulation)" { + const testing = std.testing; + var allocator = testing.allocator; + + // Create source temp directory with test files + var src_tmp = testing.tmpDir(.{}); + defer src_tmp.cleanup(); + const src_dir = src_tmp.dir; + + // Create test files + { + const file = try src_dir.createFile("main.roc", .{}); + defer file.close(); + try file.writeAll("app \"hello\" provides [main] to \"./platform\"\n\nmain = \"Hello!\"\n"); + } + { + try src_dir.makePath("platform"); + const file = try src_dir.createFile("platform/main.roc", .{}); + defer file.close(); + try file.writeAll("platform \"test\" requires { main : Str }\n"); + } + + // Bundle to memory + const file_paths = [_][]const u8{ "main.roc", "platform/main.roc" }; + var file_iter = FilePathIterator{ .paths = &file_paths }; + + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); + + const filename = try bundle.bundle( + &file_iter, + TEST_COMPRESSION_LEVEL, + &allocator, + &bundle_writer.writer, + src_dir, + null, + null, + ); + defer allocator.free(filename); + + // Parse hash from filename + const hash_str = filename[0 .. filename.len - ".tar.zst".len]; + const expected_hash = (try unbundle_mod.validateBase58Hash(hash_str)).?; + + // Now unbundle using BufferExtractWriter (same as WASM uses) + // Use arena allocator for BufferExtractWriter to simplify memory management + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + var bundle_data = bundle_writer.toArrayList(); + defer bundle_data.deinit(allocator); + + var stream_reader = std.Io.Reader.fixed(bundle_data.items); + var buffer_writer = BufferExtractWriter.init(arena_alloc); + defer buffer_writer.deinit(); + + try unbundle_mod.unbundleStream( + arena_alloc, + &stream_reader, + buffer_writer.extractWriter(), + &expected_hash, + null, + ); + + // Verify files were extracted correctly + try testing.expectEqual(@as(usize, 2), buffer_writer.files.count()); + + const main_content = buffer_writer.files.get("main.roc"); + try testing.expect(main_content != null); + try testing.expectEqualStrings("app \"hello\" provides [main] to \"./platform\"\n\nmain = \"Hello!\"\n", main_content.?.items); + + const platform_content = buffer_writer.files.get("platform/main.roc"); + try testing.expect(platform_content != null); + try testing.expectEqualStrings("platform \"test\" requires { main : Str }\n", platform_content.?.items); +} + +// Test large file unbundle - verifies multi-block zstd streaming works correctly +// zstd block_size_max is ~128KB, so we test with a 256KB file +test "unbundleStream with large file (multi-block zstd)" { + const testing = std.testing; + var allocator = testing.allocator; + + // Create source temp directory + var src_tmp = testing.tmpDir(.{}); + defer src_tmp.cleanup(); + const src_dir = src_tmp.dir; + + // Create a 256KB file (larger than zstd block_size_max of ~128KB) + const large_size = 256 * 1024; + { + const file = try src_dir.createFile("large.bin", .{}); + defer file.close(); + + // Write pattern that's easy to verify + var buf: [4096]u8 = undefined; + for (&buf, 0..) |*b, i| { + b.* = @truncate(i); + } + var written: usize = 0; + while (written < large_size) { + const to_write = @min(buf.len, large_size - written); + try file.writeAll(buf[0..to_write]); + written += to_write; + } + } + + // Bundle to memory + const file_paths = [_][]const u8{"large.bin"}; + var file_iter = FilePathIterator{ .paths = &file_paths }; + + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); + + const filename = try bundle.bundle( + &file_iter, + TEST_COMPRESSION_LEVEL, + &allocator, + &bundle_writer.writer, + src_dir, + null, + null, + ); + defer allocator.free(filename); + + // Parse hash from filename + const hash_str = filename[0 .. filename.len - ".tar.zst".len]; + const expected_hash = (try unbundle_mod.validateBase58Hash(hash_str)).?; + + // Use arena allocator for BufferExtractWriter to simplify memory management + var arena = std.heap.ArenaAllocator.init(allocator); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + // Unbundle using BufferExtractWriter + var bundle_data = bundle_writer.toArrayList(); + defer bundle_data.deinit(allocator); + + var stream_reader = std.Io.Reader.fixed(bundle_data.items); + var buffer_writer = BufferExtractWriter.init(arena_alloc); + defer buffer_writer.deinit(); + + try unbundle_mod.unbundleStream( + arena_alloc, + &stream_reader, + buffer_writer.extractWriter(), + &expected_hash, + null, + ); + + // Verify file was extracted with correct size and content + try testing.expectEqual(@as(usize, 1), buffer_writer.files.count()); + + const large_content = buffer_writer.files.get("large.bin"); + try testing.expect(large_content != null); + try testing.expectEqual(large_size, large_content.?.items.len); + + // Verify content pattern + for (large_content.?.items, 0..) |b, i| { + const expected: u8 = @truncate(i % 4096); + try testing.expectEqual(expected, b); + } +} diff --git a/src/bundle/test_streaming.zig b/src/bundle/test_streaming.zig index b76cf9a9d1..3bf7216bef 100644 --- a/src/bundle/test_streaming.zig +++ b/src/bundle/test_streaming.zig @@ -18,85 +18,90 @@ const TEST_COMPRESSION_LEVEL: c_int = 2; test "simple streaming write" { const allocator = std.testing.allocator; - var output = std.ArrayList(u8).init(allocator); - defer output.deinit(); + var output_writer: std.Io.Writer.Allocating = .init(allocator); + defer output_writer.deinit(); var allocator_copy = allocator; var writer = try streaming_writer.CompressingHashWriter.init( &allocator_copy, 3, - output.writer().any(), + &output_writer.writer, bundle.allocForZstd, bundle.freeForZstd, ); defer writer.deinit(); - try writer.writer().writeAll("Hello, world!"); + try writer.interface.writeAll("Hello, world!"); try writer.finish(); + try writer.interface.flush(); // Just check we got some output - try std.testing.expect(output.items.len > 0); + var list = output_writer.toArrayList(); + defer list.deinit(allocator); + try std.testing.expect(list.items.len > 0); } test "simple streaming read" { const allocator = std.testing.allocator; // First compress some data - var compressed = std.ArrayList(u8).init(allocator); - defer compressed.deinit(); + var compressed_writer: std.Io.Writer.Allocating = .init(allocator); + defer compressed_writer.deinit(); var allocator_copy = allocator; var writer = try streaming_writer.CompressingHashWriter.init( &allocator_copy, 3, - compressed.writer().any(), + &compressed_writer.writer, bundle.allocForZstd, bundle.freeForZstd, ); defer writer.deinit(); const test_data = "Hello, world! This is a test."; - try writer.writer().writeAll(test_data); + try writer.interface.writeAll(test_data); try writer.finish(); + try writer.interface.flush(); const hash = writer.getHash(); + var compressed_list = compressed_writer.toArrayList(); + defer compressed_list.deinit(allocator); // Now decompress it - var stream = std.io.fixedBufferStream(compressed.items); + var stream = std.Io.Reader.fixed(compressed_list.items); var allocator_copy2 = allocator; var reader = try streaming_reader.DecompressingHashReader.init( &allocator_copy2, - stream.reader().any(), + &stream, hash, bundle.allocForZstd, bundle.freeForZstd, ); defer reader.deinit(); - var decompressed = std.ArrayList(u8).init(allocator); - defer decompressed.deinit(); + var decompressed_writer: std.Io.Writer.Allocating = .init(allocator); + defer decompressed_writer.deinit(); - var buffer: [1024]u8 = undefined; - while (true) { - const n = try reader.reader().read(&buffer); - if (n == 0) break; - try decompressed.appendSlice(buffer[0..n]); - } + // Stream the data from reader to writer + _ = try reader.interface.streamRemaining(&decompressed_writer.writer); + try decompressed_writer.writer.flush(); - try std.testing.expectEqualStrings(test_data, decompressed.items); + var decompressed_list = decompressed_writer.toArrayList(); + defer decompressed_list.deinit(allocator); + try std.testing.expectEqualStrings(test_data, decompressed_list.items); } test "streaming write with exact buffer boundary" { const allocator = std.testing.allocator; - var output = std.ArrayList(u8).init(allocator); - defer output.deinit(); + var output_writer: std.Io.Writer.Allocating = .init(allocator); + defer output_writer.deinit(); var allocator_copy = allocator; var writer = try streaming_writer.CompressingHashWriter.init( &allocator_copy, 3, - output.writer().any(), + &output_writer.writer, bundle.allocForZstd, bundle.freeForZstd, ); @@ -108,60 +113,57 @@ test "streaming write with exact buffer boundary" { defer allocator.free(exact_data); @memset(exact_data, 'X'); - try writer.writer().writeAll(exact_data); + try writer.interface.writeAll(exact_data); try writer.finish(); + try writer.interface.flush(); // Just verify we got output - try std.testing.expect(output.items.len > 0); + var list = output_writer.toArrayList(); + defer list.deinit(allocator); + try std.testing.expect(list.items.len > 0); } test "streaming read with hash mismatch" { const allocator = std.testing.allocator; // First compress some data - var compressed = std.ArrayList(u8).init(allocator); - defer compressed.deinit(); + var compressed_writer: std.Io.Writer.Allocating = .init(allocator); + defer compressed_writer.deinit(); var allocator_copy = allocator; var writer = try streaming_writer.CompressingHashWriter.init( &allocator_copy, 3, - compressed.writer().any(), + &compressed_writer.writer, bundle.allocForZstd, bundle.freeForZstd, ); defer writer.deinit(); - try writer.writer().writeAll("Test data"); + try writer.interface.writeAll("Test data"); try writer.finish(); + try writer.interface.flush(); // Use wrong hash var wrong_hash: [32]u8 = undefined; @memset(&wrong_hash, 0xFF); // Try to decompress with wrong hash - var stream = std.io.fixedBufferStream(compressed.items); + var compressed_list = compressed_writer.toArrayList(); + defer compressed_list.deinit(allocator); + var stream_reader = std.Io.Reader.fixed(compressed_list.items); var allocator_copy2 = allocator; var reader = try streaming_reader.DecompressingHashReader.init( &allocator_copy2, - stream.reader().any(), + &stream_reader, wrong_hash, bundle.allocForZstd, bundle.freeForZstd, ); defer reader.deinit(); - var buffer: [1024]u8 = undefined; - while (true) { - const n = reader.reader().read(&buffer) catch |err| { - try std.testing.expectEqual(err, error.HashMismatch); - return; - }; - if (n == 0) break; - } - - // Should have gotten hash mismatch error - try std.testing.expect(false); + // verifyComplete discards remaining data and checks hash + try std.testing.expectEqual(error.HashMismatch, reader.verifyComplete()); } test "different compression levels" { @@ -174,47 +176,45 @@ test "different compression levels" { var sizes: [levels.len]usize = undefined; for (levels, 0..) |level, i| { - var output = std.ArrayList(u8).init(allocator); - defer output.deinit(); + var output_writer: std.Io.Writer.Allocating = .init(allocator); + defer output_writer.deinit(); var allocator_copy = allocator; var writer = try streaming_writer.CompressingHashWriter.init( &allocator_copy, level, - output.writer().any(), + &output_writer.writer, bundle.allocForZstd, bundle.freeForZstd, ); defer writer.deinit(); - try writer.writer().writeAll(test_data); + try writer.interface.writeAll(test_data); try writer.finish(); + try writer.interface.flush(); - sizes[i] = output.items.len; + var output_list = output_writer.toArrayList(); + defer output_list.deinit(allocator); + sizes[i] = output_list.items.len; // Verify we can decompress - var stream = std.io.fixedBufferStream(output.items); + var stream_reader = std.Io.Reader.fixed(output_list.items); var allocator_copy2 = allocator; var reader = try streaming_reader.DecompressingHashReader.init( &allocator_copy2, - stream.reader().any(), + &stream_reader, writer.getHash(), bundle.allocForZstd, bundle.freeForZstd, ); defer reader.deinit(); - var decompressed = std.ArrayList(u8).init(allocator); - defer decompressed.deinit(); + var decompressed_writer: std.Io.Writer.Allocating = .init(allocator); + defer decompressed_writer.deinit(); - var buffer: [1024]u8 = undefined; - while (true) { - const n = try reader.reader().read(&buffer); - if (n == 0) break; - try decompressed.appendSlice(buffer[0..n]); - } + _ = try reader.interface.streamRemaining(&decompressed_writer.writer); - try std.testing.expectEqualStrings(test_data, decompressed.items); + try std.testing.expectEqualStrings(test_data, decompressed_writer.written()); } // Higher compression levels should generally produce smaller output @@ -222,6 +222,60 @@ test "different compression levels" { try std.testing.expect(sizes[0] >= sizes[3] or sizes[0] - sizes[3] < 10); } +test "large data roundtrip" { + const allocator = std.testing.allocator; + + // Generate test data larger than the buffer sizes + const large_size = 1024 * 1024; + const large_data = try allocator.alloc(u8, large_size); + defer allocator.free(large_data); + for (large_data, 0..) |*b, i| { + b.* = @intCast(i % 256); + } + + // Compress + var compressed_writer: std.Io.Writer.Allocating = .init(allocator); + defer compressed_writer.deinit(); + + var allocator_copy = allocator; + var writer = try streaming_writer.CompressingHashWriter.init( + &allocator_copy, + TEST_COMPRESSION_LEVEL, + &compressed_writer.writer, + bundle.allocForZstd, + bundle.freeForZstd, + ); + defer writer.deinit(); + + try writer.interface.writeAll(large_data); + try writer.finish(); + try writer.interface.flush(); + + const hash = writer.getHash(); + const compressed_list = compressed_writer.written(); + + // Decompress + var stream = std.Io.Reader.fixed(compressed_list); + var reader = try streaming_reader.DecompressingHashReader.init( + &allocator_copy, + &stream, + hash, + bundle.allocForZstd, + bundle.freeForZstd, + ); + defer reader.deinit(); + + var decompressed_writer: std.Io.Writer.Allocating = .init(allocator); + defer decompressed_writer.deinit(); + + const size_written = try reader.interface.streamRemaining(&decompressed_writer.writer); + try reader.verifyComplete(); + try std.testing.expectEqual(large_size, size_written); + try decompressed_writer.writer.flush(); + + try std.testing.expectEqualSlices(u8, large_data, decompressed_writer.written()); +} + test "large file streaming extraction" { const allocator = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); @@ -246,8 +300,8 @@ test "large file streaming extraction" { } // Bundle it - var bundle_data = std.ArrayList(u8).init(allocator); - defer bundle_data.deinit(); + var bundle_writer: std.Io.Writer.Allocating = .init(allocator); + defer bundle_writer.deinit(); const test_util = @import("test_util.zig"); const paths = [_][]const u8{"large.bin"}; @@ -258,37 +312,15 @@ test "large file streaming extraction" { &iter, 3, &allocator_copy, - bundle_data.writer(), + &bundle_writer.writer, tmp.dir, null, null, ); defer allocator.free(filename); - // Extract to new directory - try tmp.dir.makeDir("extracted"); - var extract_dir = try tmp.dir.openDir("extracted", .{}); - - // Unbundle - this should use streaming for the 2MB file - var stream = std.io.fixedBufferStream(bundle_data.items); - var allocator_copy2 = allocator; - try bundle.unbundle(stream.reader(), extract_dir, &allocator_copy2, filename, null); - - // Verify file was extracted - const stat = try extract_dir.statFile("large.bin"); - // Due to std.tar limitations with large files, we might not get all bytes - // Just verify we got a reasonable amount (at least 100KB) - try std.testing.expect(stat.size > 100_000); - - // Verify content pattern - const verify_file = try extract_dir.openFile("large.bin", .{}); - defer verify_file.close(); - - var verify_buffer: [1024]u8 = undefined; - const bytes_read = try verify_file.read(&verify_buffer); - - // Check first 1KB has the expected pattern - for (verify_buffer[0..bytes_read], 0..) |b, i| { - try std.testing.expectEqual(@as(u8, @intCast(i % 256)), b); - } + // Just verify we successfully bundled a large file + const bundle_list = bundle_writer.written(); + try std.testing.expect(bundle_list.len > 512); // Should include header and compressed data + // Note: Full round-trip testing with unbundle is done in integration tests } diff --git a/src/canonicalize/CIR.zig b/src/canonicalize/CIR.zig index 2daa535cc9..31e9e2409a 100644 --- a/src/canonicalize/CIR.zig +++ b/src/canonicalize/CIR.zig @@ -2,12 +2,19 @@ //! This module contains type definitions and utilities used across the canonicalization IR. const std = @import("std"); +const builtin = @import("builtin"); +const build_options = @import("build_options"); const types_mod = @import("types"); const collections = @import("collections"); const base = @import("base"); const reporting = @import("reporting"); const builtins = @import("builtins"); +// Module tracing flag - enabled via `zig build -Dtrace-modules` +// On native platforms, uses std.debug.print. On WASM, tracing in CIR is disabled +// since we don't have roc_ops here (tracing is enabled in the interpreter/shim instead). +const trace_modules = if (builtin.cpu.arch == .wasm32) false else if (@hasDecl(build_options, "trace_modules")) build_options.trace_modules else false; + const CompactWriter = collections.CompactWriter; const Ident = base.Ident; const StringLiteral = base.StringLiteral; @@ -26,12 +33,88 @@ pub const Statement = @import("Statement.zig").Statement; pub const TypeAnno = @import("TypeAnnotation.zig").TypeAnno; pub const Diagnostic = @import("Diagnostic.zig").Diagnostic; +/// Indices of builtin type declarations within the Builtin module. +/// Loaded once at startup from builtin_indices.bin (generated at build time). +/// Contains both statement indices (positions within Builtin.bin) and ident indices +/// (interned identifiers for comparison without string lookups). +pub const BuiltinIndices = struct { + // Statement indices - positions within the Builtin module + bool_type: Statement.Idx, + try_type: Statement.Idx, + dict_type: Statement.Idx, + set_type: Statement.Idx, + str_type: Statement.Idx, + list_type: Statement.Idx, + box_type: Statement.Idx, + utf8_problem_type: Statement.Idx, + u8_type: Statement.Idx, + i8_type: Statement.Idx, + u16_type: Statement.Idx, + i16_type: Statement.Idx, + u32_type: Statement.Idx, + i32_type: Statement.Idx, + u64_type: Statement.Idx, + i64_type: Statement.Idx, + u128_type: Statement.Idx, + i128_type: Statement.Idx, + dec_type: Statement.Idx, + f32_type: Statement.Idx, + f64_type: Statement.Idx, + numeral_type: Statement.Idx, + + // Ident indices - simple unqualified names (e.g., "Bool", "U8") + bool_ident: Ident.Idx, + try_ident: Ident.Idx, + dict_ident: Ident.Idx, + set_ident: Ident.Idx, + str_ident: Ident.Idx, + list_ident: Ident.Idx, + box_ident: Ident.Idx, + utf8_problem_ident: Ident.Idx, + u8_ident: Ident.Idx, + i8_ident: Ident.Idx, + u16_ident: Ident.Idx, + i16_ident: Ident.Idx, + u32_ident: Ident.Idx, + i32_ident: Ident.Idx, + u64_ident: Ident.Idx, + i64_ident: Ident.Idx, + u128_ident: Ident.Idx, + i128_ident: Ident.Idx, + dec_ident: Ident.Idx, + f32_ident: Ident.Idx, + f64_ident: Ident.Idx, + numeral_ident: Ident.Idx, + // Tag idents for Try type + ok_ident: Ident.Idx, + err_ident: Ident.Idx, + + /// Convert a nominal type's ident to a NumKind, if it's a builtin numeric type. + /// This allows direct ident comparison instead of string comparison for type identification. + pub fn numKindFromIdent(self: BuiltinIndices, ident: Ident.Idx) ?NumKind { + if (ident == self.u8_ident) return .u8; + if (ident == self.i8_ident) return .i8; + if (ident == self.u16_ident) return .u16; + if (ident == self.i16_ident) return .i16; + if (ident == self.u32_ident) return .u32; + if (ident == self.i32_ident) return .i32; + if (ident == self.u64_ident) return .u64; + if (ident == self.i64_ident) return .i64; + if (ident == self.u128_ident) return .u128; + if (ident == self.i128_ident) return .i128; + if (ident == self.f32_ident) return .f32; + if (ident == self.f64_ident) return .f64; + if (ident == self.dec_ident) return .dec; + return null; + } +}; + // Type definitions for module compilation /// Represents a definition (binding of a pattern to an expression) in the CIR pub const Def = struct { pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: base.DataSpan }; + pub const Span = extern struct { span: base.DataSpan }; pattern: Pattern.Idx, expr: Expr.Idx, @@ -87,12 +170,16 @@ pub const Def = struct { } }; -/// Represents a type header (e.g., 'Maybe a' or 'Result err ok') in type annotations +/// Represents a type header (e.g., 'Maybe a' or 'Try err ok') in type annotations pub const TypeHeader = struct { pub const Idx = enum(u32) { _ }; - pub const Span = struct { start: u32, len: u32 }; + pub const Span = extern struct { start: u32, len: u32 }; + /// The fully qualified name for lookups (e.g., "Builtin.Bool" or "MyModule.Foo.Bar") name: base.Ident.Idx, + /// The name relative to the module, without the module prefix (e.g., "Bool" or "Foo.Bar"). + /// This is what should be used for NominalType.ident to avoid redundancy with origin_module. + relative_name: base.Ident.Idx, args: TypeAnno.Span, pub fn pushToSExprTree(self: *const TypeHeader, cir: anytype, tree: anytype, idx: TypeHeader.Idx) !void { @@ -126,27 +213,25 @@ pub const TypeHeader = struct { /// Represents a where clause constraint in type definitions pub const WhereClause = union(enum) { pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: base.DataSpan }; + pub const Span = extern struct { span: base.DataSpan }; - mod_method: struct { - var_name: base.Ident.Idx, + w_method: struct { + var_: TypeAnno.Idx, method_name: base.Ident.Idx, args: TypeAnno.Span, - ret_anno: TypeAnno.Idx, - external_decl: ExternalDecl.Idx, + ret: TypeAnno.Idx, }, - mod_alias: struct { - var_name: base.Ident.Idx, + w_alias: struct { + var_: TypeAnno.Idx, alias_name: base.Ident.Idx, - external_decl: ExternalDecl.Idx, }, - malformed: struct { + w_malformed: struct { diagnostic: Diagnostic.Idx, }, pub fn pushToSExprTree(self: *const WhereClause, cir: anytype, tree: anytype, idx: WhereClause.Idx) !void { switch (self.*) { - .mod_method => |method| { + .w_method => |method| { const begin = tree.beginNode(); try tree.pushStaticAtom("method"); @@ -156,11 +241,10 @@ pub const WhereClause = union(enum) { try cir.appendRegionInfoToSExprTreeFromRegion(tree, region); // Add module-of and ident information - const var_name_str = cir.getIdent(method.var_name); - try tree.pushStringPair("module-of", var_name_str); + try cir.store.getTypeAnno(method.var_).pushToSExprTree(cir, tree, method.var_); const method_name_str = cir.getIdent(method.method_name); - try tree.pushStringPair("ident", method_name_str); + try tree.pushStringPair("name", method_name_str); const attrs = tree.beginNode(); @@ -174,10 +258,10 @@ pub const WhereClause = union(enum) { try tree.endNode(args_begin, args_attrs); // Add actual return type - try cir.store.getTypeAnno(method.ret_anno).pushToSExprTree(cir, tree, method.ret_anno); + try cir.store.getTypeAnno(method.ret).pushToSExprTree(cir, tree, method.ret); try tree.endNode(begin, attrs); }, - .mod_alias => |alias| { + .w_alias => |alias| { const begin = tree.beginNode(); try tree.pushStaticAtom("alias"); @@ -186,16 +270,15 @@ pub const WhereClause = union(enum) { const region = cir.store.getRegionAt(node_idx); try cir.appendRegionInfoToSExprTreeFromRegion(tree, region); - const var_name_str = cir.getIdent(alias.var_name); - try tree.pushStringPair("module-of", var_name_str); + try cir.store.getTypeAnno(alias.var_).pushToSExprTree(cir, tree, alias.var_); const alias_name_str = cir.getIdent(alias.alias_name); - try tree.pushStringPair("ident", alias_name_str); + try tree.pushStringPair("name", alias_name_str); const attrs = tree.beginNode(); try tree.endNode(begin, attrs); }, - .malformed => |malformed| { + .w_malformed => { const begin = tree.beginNode(); try tree.pushStaticAtom("malformed"); @@ -204,7 +287,6 @@ pub const WhereClause = union(enum) { const region = cir.store.getRegionAt(node_idx); try cir.appendRegionInfoToSExprTreeFromRegion(tree, region); - _ = malformed; const attrs = tree.beginNode(); try tree.endNode(begin, attrs); }, @@ -216,25 +298,35 @@ pub const WhereClause = union(enum) { pub const Annotation = struct { pub const Idx = enum(u32) { _ }; - type_anno: TypeAnno.Idx, - signature: TypeVar, + anno: TypeAnno.Idx, + where: ?WhereClause.Span, + + pub fn pushToSExprTree(self: *const @This(), env: anytype, tree: *SExprTree, idx: Annotation.Idx) !void { + const annotation = self.*; - pub fn pushToSExprTree(self: *const Annotation, cir: anytype, tree: anytype, idx: Annotation.Idx) !void { const begin = tree.beginNode(); try tree.pushStaticAtom("annotation"); - - // Get the region for this Annotation - const node_idx: Node.Idx = @enumFromInt(@intFromEnum(idx)); - const region = cir.store.getRegionAt(node_idx); - try cir.appendRegionInfoToSExprTreeFromRegion(tree, region); - const attrs = tree.beginNode(); - const type_anno_begin = tree.beginNode(); - try tree.pushStaticAtom("declared-type"); - const type_anno_attrs = tree.beginNode(); - try cir.store.getTypeAnno(self.type_anno).pushToSExprTree(cir, tree, self.type_anno); - try tree.endNode(type_anno_begin, type_anno_attrs); + // Get the region for this Annotation + const region = env.store.getAnnotationRegion(idx); + try env.appendRegionInfoToSExprTreeFromRegion(tree, region); + + // Append annotation + try env.store.getTypeAnno(annotation.anno).pushToSExprTree(env, tree, self.anno); + + // Append where clause + if (annotation.where) |where_span| { + const where_begin = tree.beginNode(); + try tree.pushStaticAtom("where"); + const where_attrs = tree.beginNode(); + const where_clauses = env.store.sliceWhereClauses(where_span); + for (where_clauses) |clause_idx| { + const clause = env.store.getWhereClause(clause_idx); + try clause.pushToSExprTree(env, tree, clause_idx); + } + try tree.endNode(where_begin, where_attrs); + } try tree.endNode(begin, attrs); } @@ -243,7 +335,7 @@ pub const Annotation = struct { /// Represents an item exposed by a module's interface pub const ExposedItem = struct { pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: base.DataSpan }; + pub const Span = extern struct { span: base.DataSpan }; name: base.Ident.Idx, alias: ?base.Ident.Idx, @@ -268,27 +360,344 @@ pub const ExposedItem = struct { } }; -/// Represents a field in a record pattern for pattern matching -pub const PatternRecordField = struct { - pub const Idx = enum(u32) { _ }; - pub const Span = struct { start: u32, len: u32 }; +/// Represents an arbitrary precision smallish decimal value +pub const SmallDecValue = struct { + numerator: i16, + denominator_power_of_ten: u8, + + /// Convert a small dec to f64 (use for size comparisons) + pub fn toF64(self: @This()) f64 { + const numerator_f64 = @as(f64, @floatFromInt(self.numerator)); + const divisor = std.math.pow(f64, 10, @as(f64, @floatFromInt(self.denominator_power_of_ten))); + return numerator_f64 / divisor; + } + + /// Calculate the int requirements of a SmallDecValue + pub fn toFracRequirements(self: SmallDecValue) types_mod.FracRequirements { + const f64_val = self.toF64(); + return types_mod.FracRequirements{ + .fits_in_f32 = fitsInF32(f64_val), + .fits_in_dec = fitsInDec(f64_val), + }; + } + + test "SmallDecValue.toF64 - basic cases" { + // Test integer values + { + const val = SmallDecValue{ .numerator = 42, .denominator_power_of_ten = 0 }; + try std.testing.expectEqual(@as(f64, 42.0), val.toF64()); + } + + // Test decimal values + { + const val = SmallDecValue{ .numerator = 314, .denominator_power_of_ten = 2 }; + try std.testing.expectApproxEqAbs(@as(f64, 3.14), val.toF64(), 0.0001); + } + + // Test negative values + { + const val = SmallDecValue{ .numerator = -500, .denominator_power_of_ten = 3 }; + try std.testing.expectApproxEqAbs(@as(f64, -0.5), val.toF64(), 0.0001); + } + + // Test very small values + { + const val = SmallDecValue{ .numerator = 1, .denominator_power_of_ten = 10 }; + try std.testing.expectApproxEqAbs(@as(f64, 1e-10), val.toF64(), 1e-15); + } + + // Test maximum numerator + { + const val = SmallDecValue{ .numerator = 32767, .denominator_power_of_ten = 0 }; + try std.testing.expectEqual(@as(f64, 32767.0), val.toF64()); + } + + // Test minimum numerator + { + const val = SmallDecValue{ .numerator = -32768, .denominator_power_of_ten = 0 }; + try std.testing.expectEqual(@as(f64, -32768.0), val.toF64()); + } + } + + test "SmallDecValue.toFracRequirements - fits in all types" { + // Small integer - fits in everything + { + const val = SmallDecValue{ .numerator = 100, .denominator_power_of_ten = 0 }; + const req = val.toFracRequirements(); + try std.testing.expect(req.fits_in_f32); + try std.testing.expect(req.fits_in_dec); + } + + // Pi approximation - fits in everything + { + const val = SmallDecValue{ .numerator = 31416, .denominator_power_of_ten = 4 }; + const req = val.toFracRequirements(); + try std.testing.expect(req.fits_in_f32); + try std.testing.expect(req.fits_in_dec); + } + } }; +/// Check if the given f64 fits in f32 range (ignoring precision loss) +pub fn fitsInF32(f64_val: f64) bool { + // Check if it's within the range that f32 can represent. + // This includes normal, subnormal, and zero values. + // (This is a magnitude check, so take the abs value to check + // positive and negative at the same time.) + const abs_val = @abs(f64_val); + return abs_val == 0.0 or (abs_val >= std.math.floatTrueMin(f32) and abs_val <= std.math.floatMax(f32)); +} + +/// Check if a float value can be represented accurately in RocDec +pub fn fitsInDec(value: f64) bool { + // RocDec uses i128 with 18 decimal places + const max_dec_value = 170141183460469231731.0; + const min_dec_value = -170141183460469231731.0; + + return value >= min_dec_value and value <= max_dec_value; +} + /// Represents an arbitrary precision integer value pub const IntValue = struct { bytes: [16]u8, - kind: enum { - i64, - u64, + kind: IntKind, + + pub const IntKind = enum { i128, u128, - }, + }; pub fn toI128(self: IntValue) i128 { return @bitCast(self.bytes); } + + pub fn bufPrint(self: IntValue, buf: []u8) ![]u8 { + switch (self.kind) { + .i128 => { + const val: i128 = @bitCast(self.bytes); + return std.fmt.bufPrint(buf, "{d}", .{val}); + }, + .u128 => { + const val: u128 = @bitCast(self.bytes); + return std.fmt.bufPrint(buf, "{d}", .{val}); + }, + } + } + + /// Calculate the int requirements of an IntValue + pub fn toIntRequirements(self: IntValue) types_mod.IntRequirements { + var is_negated = false; + var u128_val: u128 = undefined; + + switch (self.kind) { + .i128 => { + const val: i128 = @bitCast(self.bytes); + is_negated = val < 0; + u128_val = if (val < 0) @abs(val) else @intCast(val); + }, + .u128 => { + const val: u128 = @bitCast(self.bytes); + is_negated = false; + u128_val = val; + }, + } + + // Special handling for minimum signed values + // These are the exact minimum values for each signed integer type. + // They need special handling because their absolute value is one more + // than the maximum positive value of the same signed type. + // For example: i8 range is -128 to 127, so abs(-128) = 128 doesn't fit in i8's positive range + const is_minimum_signed = is_negated and switch (u128_val) { + @as(u128, @intCast(std.math.maxInt(i8))) + 1 => true, + @as(u128, @intCast(std.math.maxInt(i16))) + 1 => true, + @as(u128, @intCast(std.math.maxInt(i32))) + 1 => true, + @as(u128, @intCast(std.math.maxInt(i64))) + 1 => true, + @as(u128, @intCast(std.math.maxInt(i128))) + 1 => true, + else => false, + }; + + // For minimum signed values, subtract 1 from the magnitude + // This makes the bit calculation work correctly with the "n-1 bits for magnitude" rule + const adjusted_val = if (is_minimum_signed) u128_val - 1 else u128_val; + const bits_needed = types_mod.Int.BitsNeeded.fromValue(adjusted_val); + return types_mod.IntRequirements{ + .sign_needed = is_negated and u128_val != 0, // -0 doesn't need a sign + .bits_needed = bits_needed.toBits(), + .is_minimum_signed = is_minimum_signed, + }; + } + + /// Calculate the frac requirements of an IntValue + pub fn toFracRequirements(self: IntValue) types_mod.FracRequirements { + // Convert to f64 for checking + const f64_val: f64 = switch (self.kind) { + .i128 => @floatFromInt(@as(i128, @bitCast(self.bytes))), + .u128 => blk: { + const val = @as(u128, @bitCast(self.bytes)); + if (val > @as(u128, 1) << 64) { + break :blk std.math.inf(f64); + } + break :blk @floatFromInt(val); + }, + }; + + // For integers, check both range AND exact representability in f32 + const fits_in_f32 = blk: { + // Check range + if (!fitsInF32(f64_val)) { + break :blk false; + } + + // Additionally check exact representability for integers + // F32 can exactly represent integers only up to 2^24 + const f32_max_exact_int = 16777216.0; // 2^24 + break :blk @abs(f64_val) <= f32_max_exact_int; + }; + + const fits_in_dec = fitsInDec(f64_val); + + return types_mod.FracRequirements{ + .fits_in_f32 = fits_in_f32, + .fits_in_dec = fits_in_dec, + }; + } }; +/// Canonical information about a number +pub const NumKind = enum { + // If this number has no restrictions + num_unbound, + + // If this number is an int with no restrictions + int_unbound, + + // If the number has an explicit suffix + u8, + i8, + u16, + i16, + u32, + i32, + u64, + i64, + u128, + i128, + f32, + f64, + dec, +}; + +/// Base-256 digit storage for Numeral values. +/// Used to construct Roc Numeral values during compile-time evaluation. +/// +/// Numeral in Roc stores: +/// - is_negative: Bool (whether there was a minus sign) +/// - digits_before_pt: List(U8) (base-256 digits before decimal point) +/// - digits_after_pt: List(U8) (base-256 digits after decimal point) +/// +/// Example: "356.517" becomes: +/// - is_negative = false +/// - digits_before_pt = [1, 100] (because 356 = 1*256 + 100) +/// - digits_after_pt = [2, 5] (because 517 = 2*256 + 5) +pub const NumeralDigits = struct { + /// Index into the shared digit byte array in ModuleEnv + digits_start: u32, + /// Number of bytes for digits_before_pt + before_pt_len: u16, + /// Number of bytes for digits_after_pt + after_pt_len: u16, + /// Whether the literal had a minus sign + is_negative: bool, + + /// Get the total length of stored digits + pub fn totalLen(self: NumeralDigits) u32 { + return @as(u32, self.before_pt_len) + @as(u32, self.after_pt_len); + } + + /// Extract digits_before_pt from the shared byte array + pub fn getDigitsBeforePt(self: NumeralDigits, digit_bytes: []const u8) []const u8 { + return digit_bytes[self.digits_start..][0..self.before_pt_len]; + } + + /// Extract digits_after_pt from the shared byte array + pub fn getDigitsAfterPt(self: NumeralDigits, digit_bytes: []const u8) []const u8 { + const after_start = self.digits_start + self.before_pt_len; + return digit_bytes[after_start..][0..self.after_pt_len]; + } + + /// Format the base-256 encoded numeral back to a human-readable decimal string. + /// Writes to the provided buffer and returns a slice of the written content. + /// Buffer should be at least 128 bytes to handle most numbers. + pub fn formatDecimal(self: NumeralDigits, digit_bytes: []const u8, buf: []u8) []const u8 { + return formatBase256ToDecimal( + self.is_negative, + self.getDigitsBeforePt(digit_bytes), + self.getDigitsAfterPt(digit_bytes), + buf, + ); + } +}; + +/// Format base-256 encoded digits to a human-readable decimal string. +/// This is useful for error messages where we need to show the user what number +/// was invalid (e.g., "The number 999999999 is not a valid U8"). +/// +/// Parameters: +/// - is_negative: whether the number had a minus sign +/// - digits_before_pt: base-256 encoded integer part +/// - digits_after_pt: base-256 encoded fractional part +/// - buf: output buffer (should be at least 128 bytes) +/// +/// Returns a slice of buf containing the formatted decimal string. +pub fn formatBase256ToDecimal( + is_negative: bool, + digits_before_pt: []const u8, + digits_after_pt: []const u8, + buf: []u8, +) []const u8 { + var writer = std.io.fixedBufferStream(buf); + const w = writer.writer(); + + // Write sign if negative + if (is_negative) w.writeAll("-") catch {}; + + // Convert base-256 integer part to decimal + var value: u128 = 0; + for (digits_before_pt) |digit| { + value = value * 256 + digit; + } + w.print("{d}", .{value}) catch {}; + + // Format fractional part if present and non-zero + if (digits_after_pt.len > 0) { + var has_nonzero = false; + for (digits_after_pt) |d| { + if (d != 0) { + has_nonzero = true; + break; + } + } + if (has_nonzero) { + w.writeAll(".") catch {}; + // Convert base-256 fractional digits to decimal + var frac: f64 = 0; + var mult: f64 = 1.0 / 256.0; + for (digits_after_pt) |digit| { + frac += @as(f64, @floatFromInt(digit)) * mult; + mult /= 256.0; + } + // Print fractional part (removing leading "0.") + var frac_buf: [32]u8 = undefined; + const frac_str = std.fmt.bufPrint(&frac_buf, "{d:.6}", .{frac}) catch "0"; + if (frac_str.len > 2 and std.mem.startsWith(u8, frac_str, "0.")) { + w.writeAll(frac_str[2..]) catch {}; + } + } + } + + return buf[0..writer.pos]; +} + // RocDec type definition (for missing export) // Must match the structure of builtins.RocDec pub const RocDec = builtins.dec.RocDec; @@ -307,13 +716,25 @@ pub fn fromF64(f: f64) ?RocDec { /// Represents an import statement in a module pub const Import = struct { - pub const Idx = enum(u32) { _ }; + pub const Idx = enum(u32) { + first = 0, + _, + }; + + /// Sentinel value indicating unresolved import (max u32) + pub const UNRESOLVED_MODULE: u32 = std.math.maxInt(u32); pub const Store = struct { /// Map from interned string idx to Import.Idx for deduplication map: std.AutoHashMapUnmanaged(base.StringLiteral.Idx, Import.Idx) = .{}, /// List of interned string IDs indexed by Import.Idx imports: collections.SafeList(base.StringLiteral.Idx) = .{}, + /// List of interned ident IDs indexed by Import.Idx (parallel to imports) + /// Used for efficient index-based lookups instead of string comparison + import_idents: collections.SafeList(base.Ident.Idx) = .{}, + /// Resolved module indices, parallel to imports list + /// Each entry is either a valid module index or UNRESOLVED_MODULE + resolved_modules: collections.SafeList(u32) = .{}, pub fn init() Store { return .{}; @@ -322,16 +743,37 @@ pub const Import = struct { pub fn deinit(self: *Store, allocator: std.mem.Allocator) void { self.map.deinit(allocator); self.imports.deinit(allocator); + self.import_idents.deinit(allocator); + self.resolved_modules.deinit(allocator); } /// Get or create an Import.Idx for the given module name. /// The module name is first checked against existing imports by comparing strings. + /// New imports are initially unresolved (UNRESOLVED_MODULE). + /// If ident_idx is provided, it will be stored for index-based lookups. pub fn getOrPut(self: *Store, allocator: std.mem.Allocator, strings: *base.StringLiteral.Store, module_name: []const u8) !Import.Idx { + return self.getOrPutWithIdent(allocator, strings, module_name, null); + } + + /// Get or create an Import.Idx for the given module name, with an associated ident. + /// The module name is first checked against existing imports by comparing strings. + /// New imports are initially unresolved (UNRESOLVED_MODULE). + /// If ident_idx is provided, it will be stored for index-based lookups. + pub fn getOrPutWithIdent(self: *Store, allocator: std.mem.Allocator, strings: *base.StringLiteral.Store, module_name: []const u8, ident_idx: ?base.Ident.Idx) !Import.Idx { // First check if we already have this module name by comparing strings for (self.imports.items.items, 0..) |existing_string_idx, i| { const existing_name = strings.get(existing_string_idx); if (std.mem.eql(u8, existing_name, module_name)) { // Found existing import with same name + // Update ident if provided and not already set + if (ident_idx) |ident| { + if (i < self.import_idents.len()) { + const current = self.import_idents.items.items[i]; + if (current.isNone()) { + self.import_idents.items.items[i] = ident; + } + } + } return @as(Import.Idx, @enumFromInt(i)); } } @@ -340,13 +782,83 @@ pub const Import = struct { const string_idx = try strings.insert(allocator, module_name); const idx = @as(Import.Idx, @enumFromInt(self.imports.len())); - // Add to both the list and the map + // Add to both the list and the map, with unresolved module initially _ = try self.imports.append(allocator, string_idx); + _ = try self.import_idents.append(allocator, ident_idx orelse base.Ident.Idx.NONE); + _ = try self.resolved_modules.append(allocator, Import.UNRESOLVED_MODULE); try self.map.put(allocator, string_idx, idx); return idx; } + /// Get the ident index for an import, or null if not set + pub fn getIdentIdx(self: *const Store, import_idx: Import.Idx) ?base.Ident.Idx { + const idx = @intFromEnum(import_idx); + if (idx >= self.import_idents.len()) return null; + const ident = self.import_idents.items.items[idx]; + if (ident.isNone()) return null; + return ident; + } + + /// Get the resolved module index for an import, or null if unresolved + pub fn getResolvedModule(self: *const Store, import_idx: Import.Idx) ?u32 { + const idx = @intFromEnum(import_idx); + if (idx >= self.resolved_modules.len()) return null; + const resolved = self.resolved_modules.items.items[idx]; + if (resolved == Import.UNRESOLVED_MODULE) return null; + return resolved; + } + + /// Set the resolved module index for an import + pub fn setResolvedModule(self: *Store, import_idx: Import.Idx, module_idx: u32) void { + const idx = @intFromEnum(import_idx); + if (idx < self.resolved_modules.len()) { + self.resolved_modules.items.items[idx] = module_idx; + } + } + + /// Resolve all imports by matching import names to module names in the provided array. + /// This sets the resolved_modules index for each import that matches a module. + /// + /// Parameters: + /// - env: The module environment containing the string store for import names + /// - available_modules: Array of module environments to match against + /// + /// For each import, this finds the module in available_modules whose module_name + /// matches the import name and sets the resolved index accordingly. + /// + /// For package-qualified imports like "pf.Stdout", this also tries to match the + /// base module name ("Stdout") if the full qualified name doesn't match. + pub fn resolveImports(self: *Store, env: anytype, available_modules: []const *const @import("ModuleEnv.zig")) void { + const import_count: usize = @intCast(self.imports.len()); + for (0..import_count) |i| { + const import_idx: Import.Idx = @enumFromInt(i); + const str_idx = self.imports.items.items[i]; + const import_name = env.common.getString(str_idx); + + // For package-qualified imports like "pf.Stdout", extract the base module name + const base_name = if (std.mem.lastIndexOf(u8, import_name, ".")) |dot_pos| + import_name[dot_pos + 1 ..] + else + import_name; + + // Find matching module in available_modules by comparing module names + for (available_modules, 0..) |module_env, module_idx| { + // Try exact match first, then base name match for package-qualified imports + if (std.mem.eql(u8, module_env.module_name, import_name) or + std.mem.eql(u8, module_env.module_name, base_name)) + { + self.setResolvedModule(import_idx, @intCast(module_idx)); + + if (comptime trace_modules) { + std.debug.print("[TRACE-MODULES] resolveImports: \"{s}\" -> module_idx={d} (matched \"{s}\")\n", .{ import_name, module_idx, module_env.module_name }); + } + break; + } + } + } + } + /// Serialize this Store to the given CompactWriter. The resulting Store /// in the writer's buffer will have offsets instead of pointers. Calling any /// methods on it or dereferencing its internal "pointers" (which are now @@ -363,6 +875,8 @@ pub const Import = struct { offset_self.* = .{ .map = .{}, // Map will be empty after deserialization (only used for deduplication during insertion) .imports = (try self.imports.serialize(allocator, writer)).*, + .import_idents = (try self.import_idents.serialize(allocator, writer)).*, + .resolved_modules = (try self.resolved_modules.serialize(allocator, writer)).*, }; return @constCast(offset_self); @@ -371,12 +885,18 @@ pub const Import = struct { /// Add the given offset to the memory addresses of all pointers in `self`. pub fn relocate(self: *Store, offset: isize) void { self.imports.relocate(offset); + self.import_idents.relocate(offset); + self.resolved_modules.relocate(offset); } - pub const Serialized = struct { + /// Uses extern struct to guarantee consistent field layout across optimization levels. + pub const Serialized = extern struct { // Placeholder to match Store size - not serialized - map: std.AutoHashMapUnmanaged(base.StringLiteral.Idx, Import.Idx) = .{}, + // Reserve space for hashmap (3 pointers for unmanaged hashmap internals) + map: [3]u64, imports: collections.SafeList(base.StringLiteral.Idx).Serialized, + import_idents: collections.SafeList(base.Ident.Idx).Serialized, + resolved_modules: collections.SafeList(u32).Serialized, /// Serialize a Store into this Serialized struct, appending data to the writer pub fn serialize( @@ -387,22 +907,32 @@ pub const Import = struct { ) std.mem.Allocator.Error!void { // Serialize the imports SafeList try self.imports.serialize(&store.imports, allocator, writer); + // Serialize the import_idents SafeList + try self.import_idents.serialize(&store.import_idents, allocator, writer); + // Serialize the resolved_modules SafeList + try self.resolved_modules.serialize(&store.resolved_modules, allocator, writer); + + // Set map to all zeros; the space needs to be here, + // but the map will be rebuilt during deserialization. + self.map = .{ 0, 0, 0 }; // Note: The map is not serialized as it's only used for deduplication during insertion } /// Deserialize this Serialized struct into a Store - pub fn deserialize(self: *Serialized, offset: i64, allocator: std.mem.Allocator) *Store { + pub fn deserialize(self: *Serialized, offset: i64, allocator: std.mem.Allocator) std.mem.Allocator.Error!*Store { // Overwrite ourself with the deserialized version, and return our pointer after casting it to Store. const store = @as(*Store, @ptrFromInt(@intFromPtr(self))); store.* = .{ .map = .{}, // Will be repopulated below .imports = self.imports.deserialize(offset).*, + .import_idents = self.import_idents.deserialize(offset).*, + .resolved_modules = self.resolved_modules.deserialize(offset).*, }; // Pre-allocate the exact capacity needed for the map const import_count = store.imports.items.items.len; - store.map.ensureTotalCapacity(allocator, @intCast(import_count)) catch unreachable; + try store.map.ensureTotalCapacity(allocator, @intCast(import_count)); // Repopulate the map - we know there's enough capacity since we // are deserializing from a Serialized struct @@ -420,7 +950,7 @@ pub const Import = struct { /// Represents a field in a record expression pub const RecordField = struct { pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: base.DataSpan }; + pub const Span = extern struct { span: base.DataSpan }; name: base.Ident.Idx, value: Expr.Idx, @@ -451,7 +981,7 @@ pub const ExternalDecl = struct { region: Region, pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: base.DataSpan }; + pub const Span = extern struct { span: base.DataSpan }; /// A safe list of external declarations pub const SafeList = collections.SafeList(ExternalDecl); @@ -513,7 +1043,6 @@ pub fn isCastable(comptime T: type) bool { TypeAnno.RecordField.Idx, ExposedItem.Idx, Expr.Match.BranchPattern.Idx, - PatternRecordField.Idx, Node.Idx, TypeVar, => true, diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 0c64d3f9dd..265472df06 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -4,6 +4,8 @@ //! constructs into a simplified, normalized form suitable for type inference. const std = @import("std"); +const builtin = @import("builtin"); +const build_options = @import("build_options"); const testing = std.testing; const base = @import("base"); const parse = @import("parse"); @@ -12,6 +14,8 @@ const types = @import("types"); const builtins = @import("builtins"); const tracy = @import("tracy"); +const trace_modules = if (builtin.cpu.arch == .wasm32) false else if (@hasDecl(build_options, "trace_modules")) build_options.trace_modules else false; + const CIR = @import("CIR.zig"); const Scope = @import("Scope.zig"); @@ -24,60 +28,54 @@ const DataSpan = base.DataSpan; const ModuleEnv = @import("ModuleEnv.zig"); const Node = @import("Node.zig"); -/// Both the canonicalized expression and any free variables -/// -/// We keep track of the free variables as we go so we can union these -/// in our Lambda's in a single forward pass during canonicalization. -pub const CanonicalizedExpr = struct { - idx: Expr.Idx, - free_vars: ?[]Pattern.Idx, - - pub fn get_idx(self: @This()) Expr.Idx { - return self.idx; - } - - pub fn maybe_expr_get_idx(self: ?@This()) ?Expr.Idx { - if (self != null) { - return self.?.idx; - } else { - return null; - } - } +/// Information about an auto-imported module type +pub const AutoImportedType = struct { + env: *const ModuleEnv, + /// Optional statement index for types (e.g., Builtin.Bool, Builtin.Num.U8) + /// When set, this points directly to the type declaration, avoiding string lookups + statement_idx: ?CIR.Statement.Idx = null, + /// The fully qualified type identifier (e.g., "Builtin.Str" for Str, "Builtin.Num.U8" for U8) + /// Used for looking up members like U8.to_i16 -> "Builtin.Num.U8.to_i16" + qualified_type_ident: Ident.Idx, }; -const TypeVarProblemKind = enum { - unused_type_var, - type_var_marked_unused, - type_var_ending_in_underscore, -}; - -const TypeVarProblem = struct { - ident: Ident.Idx, - problem: TypeVarProblemKind, - ast_anno: AST.TypeAnno.Idx, +/// Information about a placeholder identifier, tracking its component parts +const PlaceholderInfo = struct { + parent_qualified_idx: Ident.Idx, // The qualified parent type name (e.g., "Module.Foo.Bar") + item_name_idx: Ident.Idx, // The unqualified item name (e.g., "baz") }; env: *ModuleEnv, parse_ir: *AST, -scopes: std.ArrayListUnmanaged(Scope) = .{}, -/// Special scope for tracking exposed items from module header -exposed_scope: Scope = undefined, +/// Track whether we're in statement position (true) or expression position (false) +/// Statement position: if without else is OK (default) +/// Expression position: if without else is ERROR (explicitly set in assignments, etc.) +in_statement_position: bool = true, +scopes: std.ArrayList(Scope) = .{}, +/// Special scope for rigid type variables in annotations +type_vars_scope: base.Scratch(TypeVarScope), +/// Set of identifiers exposed from this module header (values not used) +exposed_idents: std.AutoHashMapUnmanaged(Ident.Idx, void) = .{}, +/// Set of types exposed from this module header (values not used) +exposed_types: std.AutoHashMapUnmanaged(Ident.Idx, void) = .{}, /// Track exposed identifiers by text to handle changing indices exposed_ident_texts: std.StringHashMapUnmanaged(Region) = .{}, /// Track exposed types by text to handle changing indices exposed_type_texts: std.StringHashMapUnmanaged(Region) = .{}, -/// Special scope for unqualified nominal tags (e.g., True, False) -unqualified_nominal_tags: std.StringHashMapUnmanaged(Statement.Idx) = .{}, +/// Track which identifiers in the current scope are placeholders (not yet replaced with real definitions) +/// This is empty for 99% of files; only used during multi-phase canonicalization (mainly Builtin.roc) +/// Maps the fully qualified placeholder ident to its component parts for hierarchical registration +placeholder_idents: std.AutoHashMapUnmanaged(Ident.Idx, PlaceholderInfo) = .{}, /// Stack of function regions for tracking var reassignment across function boundaries -function_regions: std.ArrayListUnmanaged(Region), +function_regions: std.array_list.Managed(Region), /// Maps var patterns to the function region they were declared in var_function_regions: std.AutoHashMapUnmanaged(Pattern.Idx, Region), /// Set of pattern indices that are vars var_patterns: std.AutoHashMapUnmanaged(Pattern.Idx, void), /// Tracks which pattern indices have been used/referenced used_patterns: std.AutoHashMapUnmanaged(Pattern.Idx, void), -/// Map of module name strings to their ModuleEnv pointers for import validation -module_envs: ?*const std.StringHashMap(*ModuleEnv), +/// Map of module name identifiers to their type information for import validation +module_envs: ?*const std.AutoHashMap(Ident.Idx, AutoImportedType), /// Map from module name string to Import.Idx for tracking unique imports import_indices: std.StringHashMapUnmanaged(Import.Idx), /// Scratch type variables @@ -96,6 +94,12 @@ scratch_seen_record_fields: base.Scratch(SeenRecordField), scratch_tags: base.Scratch(types.Tag), /// Scratch free variables scratch_free_vars: base.Scratch(Pattern.Idx), +/// Scratch captures (free variables being collected) +scratch_captures: base.Scratch(Pattern.Idx), +/// Scratch bound variables (for filtering out locally-bound vars from captures) +scratch_bound_vars: base.Scratch(Pattern.Idx), +/// Counter for generating unique malformed import placeholder names +malformed_import_count: u32 = 0, const Ident = base.Ident; const Region = base.Region; @@ -105,6 +109,7 @@ const CalledVia = base.CalledVia; const TypeVar = types.Var; const Content = types.Content; +const Flex = types.Flex; const FlatType = types.FlatType; const Num = types.Num; @@ -129,33 +134,53 @@ const RecordField = CIR.RecordField; /// Struct to track fields that have been seen before during canonicalization const SeenRecordField = struct { ident: base.Ident.Idx, region: base.Region }; -/// The idx of the builtin Bool -/// The idx of the builtin Bool pattern (not used for type checking - use BUILTIN_BOOL_TYPE instead) -pub const BUILTIN_BOOL: Pattern.Idx = @enumFromInt(0); -/// The idx of the builtin Bool type declaration (use this for type checking) -pub var BUILTIN_BOOL_TYPE: Statement.Idx = undefined; -/// The idx of the builtin Box -pub const BUILTIN_BOX: Pattern.Idx = @enumFromInt(1); -/// The idx of the builtin Decode -pub const BUILTIN_DECODE: Pattern.Idx = @enumFromInt(2); -/// The idx of the builtin Dict -pub const BUILTIN_DICT: Pattern.Idx = @enumFromInt(3); -/// The idx of the builtin Encode -pub const BUILTIN_ENCODE: Pattern.Idx = @enumFromInt(4); -/// The idx of the builtin Hash -pub const BUILTIN_HASH: Pattern.Idx = @enumFromInt(5); -/// The idx of the builtin Inspect -pub const BUILTIN_INSPECT: Pattern.Idx = @enumFromInt(6); -/// The idx of the builtin List -pub const BUILTIN_LIST: Pattern.Idx = @enumFromInt(7); -/// The idx of the builtin Num -pub const BUILTIN_NUM: Pattern.Idx = @enumFromInt(8); -/// The idx of the builtin Result -pub const BUILTIN_RESULT: Pattern.Idx = @enumFromInt(9); -/// The idx of the builtin Set -pub const BUILTIN_SET: Pattern.Idx = @enumFromInt(10); -/// The idx of the builtin Str -pub const BUILTIN_STR: Pattern.Idx = @enumFromInt(11); +/// Both the canonicalized expression and any free variables +/// +/// We keep track of the free variables as we go so we can union these +/// in our Lambda's in a single forward pass during canonicalization. +pub const CanonicalizedExpr = struct { + idx: Expr.Idx, + free_vars: DataSpan, // This is a span into scratch_free_vars + + pub fn get_idx(self: @This()) Expr.Idx { + return self.idx; + } + + pub fn maybe_expr_get_idx(self: ?@This()) ?Expr.Idx { + if (self != null) { + return self.?.idx; + } else { + return null; + } + } +}; + +const TypeVarProblemKind = enum { + unused_type_var, + type_var_marked_unused, + type_var_starting_with_dollar, +}; + +const TypeVarProblem = struct { + ident: Ident.Idx, + problem: TypeVarProblemKind, + ast_anno: AST.TypeAnno.Idx, +}; + +const ModuleFoundStatus = enum { + module_was_found, + module_not_found, +}; + +const TypeBindingLocation = struct { + scope_index: usize, + binding: *Scope.TypeBinding, +}; + +const TypeBindingLocationConst = struct { + scope_index: usize, + binding: *const Scope.TypeBinding, +}; /// Deinitialize canonicalizer resources pub fn deinit( @@ -163,10 +188,12 @@ pub fn deinit( ) void { const gpa = self.env.gpa; - self.exposed_scope.deinit(gpa); + self.type_vars_scope.deinit(); + self.exposed_idents.deinit(gpa); + self.exposed_types.deinit(gpa); self.exposed_ident_texts.deinit(gpa); self.exposed_type_texts.deinit(gpa); - self.unqualified_nominal_tags.deinit(gpa); + self.placeholder_idents.deinit(gpa); for (0..self.scopes.items.len) |i| { var scope = &self.scopes.items[i]; @@ -174,23 +201,30 @@ pub fn deinit( } self.scopes.deinit(gpa); - self.function_regions.deinit(gpa); + self.function_regions.deinit(); self.var_function_regions.deinit(gpa); self.var_patterns.deinit(gpa); self.used_patterns.deinit(gpa); - self.scratch_vars.deinit(gpa); - self.scratch_idents.deinit(gpa); - self.scratch_type_var_validation.deinit(gpa); - self.scratch_type_var_problems.deinit(gpa); - self.scratch_record_fields.deinit(gpa); - self.scratch_seen_record_fields.deinit(gpa); + self.scratch_vars.deinit(); + self.scratch_idents.deinit(); + self.scratch_type_var_validation.deinit(); + self.scratch_type_var_problems.deinit(); + self.scratch_record_fields.deinit(); + self.scratch_seen_record_fields.deinit(); self.import_indices.deinit(gpa); - self.scratch_tags.deinit(gpa); - self.scratch_free_vars.deinit(gpa); + self.scratch_tags.deinit(); + self.scratch_free_vars.deinit(); + self.scratch_captures.deinit(); + self.scratch_bound_vars.deinit(); } -pub fn init(env: *ModuleEnv, parse_ir: *AST, module_envs: ?*const std.StringHashMap(*ModuleEnv)) std.mem.Allocator.Error!Self { +/// Options for initializing the canonicalizer. +pub fn init( + env: *ModuleEnv, + parse_ir: *AST, + module_envs: ?*const std.AutoHashMap(Ident.Idx, AutoImportedType), +) std.mem.Allocator.Error!Self { const gpa = env.gpa; // Create the canonicalizer with scopes @@ -198,7 +232,7 @@ pub fn init(env: *ModuleEnv, parse_ir: *AST, module_envs: ?*const std.StringHash .env = env, .parse_ir = parse_ir, .scopes = .{}, - .function_regions = std.ArrayListUnmanaged(Region){}, + .function_regions = std.array_list.Managed(Region).init(gpa), .var_function_regions = std.AutoHashMapUnmanaged(Pattern.Idx, Region){}, .var_patterns = std.AutoHashMapUnmanaged(Pattern.Idx, void){}, .used_patterns = std.AutoHashMapUnmanaged(Pattern.Idx, void){}, @@ -210,324 +244,152 @@ pub fn init(env: *ModuleEnv, parse_ir: *AST, module_envs: ?*const std.StringHash .scratch_type_var_problems = try base.Scratch(TypeVarProblem).init(gpa), .scratch_record_fields = try base.Scratch(types.RecordField).init(gpa), .scratch_seen_record_fields = try base.Scratch(SeenRecordField).init(gpa), - .exposed_scope = Scope.init(false), + .type_vars_scope = try base.Scratch(TypeVarScope).init(gpa), .scratch_tags = try base.Scratch(types.Tag).init(gpa), - .unqualified_nominal_tags = std.StringHashMapUnmanaged(Statement.Idx){}, .scratch_free_vars = try base.Scratch(Pattern.Idx).init(gpa), + .scratch_captures = try base.Scratch(Pattern.Idx).init(gpa), + .scratch_bound_vars = try base.Scratch(Pattern.Idx).init(gpa), }; // Top-level scope is not a function boundary try result.scopeEnter(gpa, false); - // Simulate the builtins by adding to both the NodeStore and Scopes - // Not sure if this is how we want to do it long term, but want something to - // make a start on canonicalization. + // Set up auto-imported builtin types (Bool, Try, Dict, Set) + try result.setupAutoImportedBuiltinTypes(env, gpa, module_envs); + + const scratch_statements_start = result.env.store.scratch.?.statements.top(); + + result.env.builtin_statements = try result.env.store.statementSpanFrom(scratch_statements_start); // Assert that the node store is completely empty env.debugAssertArraysInSync(); - // Add builtinss (eventually will be gotten from builtins roc files) - try result.addBuiltin(env, "Bool", BUILTIN_BOOL); - try result.addBuiltin(env, "Box", BUILTIN_BOX); - try result.addBuiltin(env, "Decode", BUILTIN_DECODE); - try result.addBuiltin(env, "Dict", BUILTIN_DICT); - try result.addBuiltin(env, "Encode", BUILTIN_ENCODE); - try result.addBuiltin(env, "Hash", BUILTIN_HASH); - try result.addBuiltin(env, "Inspect", BUILTIN_INSPECT); - try result.addBuiltin(env, "List", BUILTIN_LIST); - try result.addBuiltin(env, "Num", BUILTIN_NUM); - try result.addBuiltin(env, "Result", BUILTIN_RESULT); - try result.addBuiltin(env, "Set", BUILTIN_SET); - try result.addBuiltin(env, "Str", BUILTIN_STR); - - // Assert that the node store has the 12 builtin types - env.debugAssertArraysInSync(); - - // Add built-in types to the type scope - // TODO: These should ultimately come from the platform/builtin files rather than being hardcoded - try result.addBuiltinTypeBool(env); - try result.addBuiltinTypeList(env); - try result.addBuiltinTypeBox(env); - try result.addBuiltinTypeResult(env); - - _ = try result.addBuiltinType(env, "Str", .{ .structure = .str }); - _ = try result.addBuiltinType(env, "U8", .{ .structure = .{ .num = types.Num.int_u8 } }); - _ = try result.addBuiltinType(env, "U16", .{ .structure = .{ .num = types.Num.int_u16 } }); - _ = try result.addBuiltinType(env, "U32", .{ .structure = .{ .num = types.Num.int_u32 } }); - _ = try result.addBuiltinType(env, "U64", .{ .structure = .{ .num = types.Num.int_u64 } }); - _ = try result.addBuiltinType(env, "U128", .{ .structure = .{ .num = types.Num.int_u128 } }); - _ = try result.addBuiltinType(env, "I8", .{ .structure = .{ .num = types.Num.int_i8 } }); - _ = try result.addBuiltinType(env, "I16", .{ .structure = .{ .num = types.Num.int_i16 } }); - _ = try result.addBuiltinType(env, "I32", .{ .structure = .{ .num = types.Num.int_i32 } }); - _ = try result.addBuiltinType(env, "I64", .{ .structure = .{ .num = types.Num.int_i64 } }); - _ = try result.addBuiltinType(env, "I128", .{ .structure = .{ .num = types.Num.int_i128 } }); - _ = try result.addBuiltinType(env, "F32", .{ .structure = .{ .num = types.Num.frac_f32 } }); - _ = try result.addBuiltinType(env, "F64", .{ .structure = .{ .num = types.Num.frac_f64 } }); - _ = try result.addBuiltinType(env, "Dec", .{ .structure = .{ .num = types.Num.frac_dec } }); - _ = try result.addBuiltinType(env, "Dict", .{ .flex_var = null }); - _ = try result.addBuiltinType(env, "Set", .{ .flex_var = null }); - return result; } -// builtins // - -fn addBuiltin(self: *Self, ir: *ModuleEnv, ident_text: []const u8, idx: Pattern.Idx) std.mem.Allocator.Error!void { - const gpa = ir.gpa; - const ident_add = try ir.insertIdent(base.Ident.for_text(ident_text)); - const pattern_idx_add = try ir.addPatternAndTypeVar(Pattern{ .assign = .{ .ident = ident_add } }, Content{ .flex_var = null }, Region.zero()); - _ = try self.scopeIntroduceInternal(gpa, .ident, ident_add, pattern_idx_add, false, true); - std.debug.assert(idx == pattern_idx_add); -} - -/// Stub builtin types. Currently sets every type to be a nominal type -/// This should be replaced by real builtins eventually -fn addBuiltinType(self: *Self, ir: *ModuleEnv, type_name: []const u8, content: types.Content) std.mem.Allocator.Error!Statement.Idx { - const gpa = ir.gpa; - const type_ident = try ir.insertIdent(base.Ident.for_text(type_name)); - - // Create a type header for the built-in type - const header_idx = try ir.addTypeHeaderAndTypeVar(.{ - .name = type_ident, - .args = .{ .span = .{ .start = 0, .len = 0 } }, // No type parameters for built-ins - }, .{ .flex_var = null }, Region.zero()); - - // Create a type annotation that refers to itself (built-in types are primitive) - const anno_idx = try ir.addTypeAnnoAndTypeVar(.{ .ty = .{ - .symbol = type_ident, - } }, content, Region.zero()); - const anno_var = ModuleEnv.castIdx(TypeAnno.Idx, TypeVar, anno_idx); - - // Create the type declaration statement - const type_decl_stmt = Statement{ - .s_nominal_decl = .{ .header = header_idx, .anno = anno_idx }, +/// Populate module_envs map with auto-imported builtin types. +/// This function is called BEFORE Can.init() by both production and test environments +/// to ensure they use identical module setup logic. +/// +/// Adds Bool, Try, Dict, Set, Str, and numeric types from the Builtin module to module_envs. +pub fn populateModuleEnvs( + module_envs_map: *std.AutoHashMap(Ident.Idx, AutoImportedType), + calling_module_env: *ModuleEnv, + builtin_module_env: *const ModuleEnv, + builtin_indices: CIR.BuiltinIndices, +) !void { + // All auto-imported types with their statement index and fully-qualified ident + // Top-level types: "Builtin.Bool", "Builtin.Str", etc. + // Nested types under Num: "Builtin.Num.U8", etc. + // + // Note: builtin_indices.*_ident values are indices into the builtin module's ident store. + // We need to get the text and re-insert into the calling module's store since + // Ident.Idx values are not transferable between stores. + const builtin_types = .{ + .{ "Bool", builtin_indices.bool_type, builtin_indices.bool_ident }, + .{ "Try", builtin_indices.try_type, builtin_indices.try_ident }, + .{ "Dict", builtin_indices.dict_type, builtin_indices.dict_ident }, + .{ "Set", builtin_indices.set_type, builtin_indices.set_ident }, + .{ "Str", builtin_indices.str_type, builtin_indices.str_ident }, + .{ "List", builtin_indices.list_type, builtin_indices.list_ident }, + .{ "Box", builtin_indices.box_type, builtin_indices.box_ident }, + .{ "Utf8Problem", builtin_indices.utf8_problem_type, builtin_indices.utf8_problem_ident }, + .{ "U8", builtin_indices.u8_type, builtin_indices.u8_ident }, + .{ "I8", builtin_indices.i8_type, builtin_indices.i8_ident }, + .{ "U16", builtin_indices.u16_type, builtin_indices.u16_ident }, + .{ "I16", builtin_indices.i16_type, builtin_indices.i16_ident }, + .{ "U32", builtin_indices.u32_type, builtin_indices.u32_ident }, + .{ "I32", builtin_indices.i32_type, builtin_indices.i32_ident }, + .{ "U64", builtin_indices.u64_type, builtin_indices.u64_ident }, + .{ "I64", builtin_indices.i64_type, builtin_indices.i64_ident }, + .{ "U128", builtin_indices.u128_type, builtin_indices.u128_ident }, + .{ "I128", builtin_indices.i128_type, builtin_indices.i128_ident }, + .{ "Dec", builtin_indices.dec_type, builtin_indices.dec_ident }, + .{ "F32", builtin_indices.f32_type, builtin_indices.f32_ident }, + .{ "F64", builtin_indices.f64_type, builtin_indices.f64_ident }, + .{ "Numeral", builtin_indices.numeral_type, builtin_indices.numeral_ident }, }; - const type_decl_idx = try ir.addStatementAndTypeVar( - type_decl_stmt, - try ir.types.mkAlias(types.TypeIdent{ .ident_idx = type_ident }, anno_var, &.{}), - Region.zero(), - ); + inline for (builtin_types) |type_info| { + const type_name = type_info[0]; + const statement_idx = type_info[1]; + const builtin_qualified_ident = type_info[2]; - // Add to scope without any error checking (built-ins are always valid) - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.put(gpa, .type_decl, type_ident, type_decl_idx); + // Get the qualified ident text from the builtin module and re-insert into calling module + const qualified_text = builtin_module_env.getIdent(builtin_qualified_ident); + const qualified_ident = try calling_module_env.insertIdent(base.Ident.for_text(qualified_text)); - return type_decl_idx; + const type_ident = try calling_module_env.insertIdent(base.Ident.for_text(type_name)); + try module_envs_map.put(type_ident, .{ + .env = builtin_module_env, + .statement_idx = statement_idx, + .qualified_type_ident = qualified_ident, + }); + } } -/// Creates `Result(ok, err) := [Ok(ok), Err(err)]` -fn addBuiltinTypeResult(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!void { - const gpa = ir.gpa; - const type_ident = try ir.insertIdent(base.Ident.for_text("Result")); - const a_ident = try ir.insertIdent(base.Ident.for_text("ok")); - const b_ident = try ir.insertIdent(base.Ident.for_text("err")); +/// Set up auto-imported builtin types (Bool, Try, Dict, Set, Str, and numeric types) from the Builtin module. +/// Used for all modules EXCEPT Builtin itself. +pub fn setupAutoImportedBuiltinTypes( + self: *Self, + env: *ModuleEnv, + gpa: std.mem.Allocator, + module_envs: ?*const std.AutoHashMap(Ident.Idx, AutoImportedType), +) std.mem.Allocator.Error!void { + if (module_envs) |envs_map| { + const zero_region = Region{ .start = Region.Position.zero(), .end = Region.Position.zero() }; + const current_scope = &self.scopes.items[0]; - // Create a type header for the built-in type - const header_idx = try ir.addTypeHeaderAndTypeVar(.{ - .name = type_ident, - .args = .{ .span = .{ .start = 0, .len = 0 } }, // No type parameters for built-ins - }, .{ .flex_var = null }, Region.zero()); - const header_node_idx = ModuleEnv.nodeIdxFrom(header_idx); + // NOTE: Auto-imported types come from the Builtin module. + // We add "Builtin" to env.imports so that type checking can find it, + // but compile_package.zig has special handling to not try parsing it as a local file. - // Create a type annotation that refers to itself (built-in types are primitive) - const ext_var = try ir.addTypeSlotAndTypeVar( - header_node_idx, - Content{ .structure = .empty_tag_union }, - Region.zero(), - TypeVar, - ); - const a_rigid = try ir.addTypeSlotAndTypeVar( - header_node_idx, - .{ .rigid_var = a_ident }, - Region.zero(), - TypeVar, - ); - const b_rigid = try ir.addTypeSlotAndTypeVar( - header_node_idx, - .{ .rigid_var = b_ident }, - Region.zero(), - TypeVar, - ); - const anno_idx = try ir.addTypeAnnoAndTypeVar( - .{ .ty = .{ .symbol = type_ident } }, - try ir.types.mkResult(gpa, ir.getIdentStore(), a_rigid, b_rigid, ext_var), - Region.zero(), - ); - const anno_var = ModuleEnv.castIdx(TypeAnno.Idx, TypeVar, anno_idx); + const builtin_ident = try env.insertIdent(base.Ident.for_text("Builtin")); + const builtin_import_idx = try self.env.imports.getOrPutWithIdent( + gpa, + self.env.common.getStringStore(), + "Builtin", + builtin_ident, + ); - // Create the type declaration statement - const type_decl_stmt = Statement{ - .s_nominal_decl = .{ .header = header_idx, .anno = anno_idx }, - }; + const builtin_types = [_][]const u8{ "Bool", "Try", "Dict", "Set", "Str", "U8", "I8", "U16", "I16", "U32", "I32", "U64", "I64", "U128", "I128", "Dec", "F32", "F64", "Numeral" }; + for (builtin_types) |type_name_text| { + const type_ident = try env.insertIdent(base.Ident.for_text(type_name_text)); + if (envs_map.get(type_ident)) |type_entry| { + const target_node_idx = if (type_entry.statement_idx) |stmt_idx| + type_entry.env.getExposedNodeIndexByStatementIdx(stmt_idx) + else + null; - const type_decl_idx = try ir.addStatementAndTypeVar( - type_decl_stmt, - try ir.types.mkNominal( - types.TypeIdent{ .ident_idx = type_ident }, - anno_var, - &.{ a_rigid, b_rigid }, - try ir.insertIdent(base.Ident.for_text(ir.module_name)), - ), - Region.zero(), - ); + try current_scope.type_bindings.put(gpa, type_ident, Scope.TypeBinding{ + .external_nominal = .{ + .module_ident = builtin_ident, + .original_ident = type_ident, + .target_node_idx = target_node_idx, + .import_idx = builtin_import_idx, + .origin_region = zero_region, + .module_not_found = false, + }, + }); + } + } - // Add to scope without any error checking (built-ins are always valid) - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.put(gpa, .type_decl, type_ident, type_decl_idx); + const primitive_builtins = [_][]const u8{ "List", "Box" }; + for (primitive_builtins) |type_name_text| { + const type_ident = try env.insertIdent(base.Ident.for_text(type_name_text)); - try ir.redirectTypeTo(Pattern.Idx, BUILTIN_RESULT, ModuleEnv.varFrom(type_decl_idx)); - - // Add True and False to unqualified_nominal_tags - // TODO: in the future, we should have hardcoded constants for these. - try self.unqualified_nominal_tags.put(gpa, "Ok", type_decl_idx); - try self.unqualified_nominal_tags.put(gpa, "Err", type_decl_idx); -} - -/// Creates `List(a) : (a)` -fn addBuiltinTypeList(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!void { - const gpa = ir.gpa; - const type_ident = try ir.insertIdent(base.Ident.for_text("List")); - const elem_ident = try ir.insertIdent(base.Ident.for_text("item")); - - // Create a type header for the built-in type - const header_idx = try ir.addTypeHeaderAndTypeVar(.{ - .name = type_ident, - .args = .{ .span = .{ .start = 0, .len = 0 } }, // No type parameters for built-ins - }, .{ .flex_var = null }, Region.zero()); - const header_node_idx = ModuleEnv.nodeIdxFrom(header_idx); - - // Create a type annotation that refers to itself (built-in types are primitive) - const elem_var = try ir.addTypeSlotAndTypeVar( - header_node_idx, - Content{ .rigid_var = elem_ident }, - Region.zero(), - TypeVar, - ); - const anno_idx = try ir.addTypeAnnoAndTypeVar(.{ .ty = .{ - .symbol = type_ident, - } }, .{ .structure = .{ .list = elem_var } }, Region.zero()); - const anno_var = ModuleEnv.castIdx(TypeAnno.Idx, TypeVar, anno_idx); - - // Create the type declaration statement - const type_decl_stmt = Statement{ - .s_alias_decl = .{ .header = header_idx, .anno = anno_idx }, - }; - - const type_decl_idx = try ir.addStatementAndTypeVar( - type_decl_stmt, - try ir.types.mkAlias( - types.TypeIdent{ .ident_idx = type_ident }, - anno_var, - &.{elem_var}, - ), - Region.zero(), - ); - - // Add to scope without any error checking (built-ins are always valid) - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.put(gpa, .type_decl, type_ident, type_decl_idx); - - try ir.redirectTypeTo(Pattern.Idx, BUILTIN_LIST, ModuleEnv.varFrom(type_decl_idx)); -} - -/// Creates `Box(a) : (a)` -fn addBuiltinTypeBox(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!void { - const gpa = ir.gpa; - const type_ident = try ir.insertIdent(base.Ident.for_text("Box")); - const elem_ident = try ir.insertIdent(base.Ident.for_text("item")); - - // Create a type header for the built-in type - const header_idx = try ir.addTypeHeaderAndTypeVar(.{ - .name = type_ident, - .args = .{ .span = .{ .start = 0, .len = 0 } }, // No type parameters for built-ins - }, .{ .flex_var = null }, Region.zero()); - const header_node_idx = ModuleEnv.nodeIdxFrom(header_idx); - - // Create a type annotation that refers to itself (built-in types are primitive) - const elem_var = try ir.addTypeSlotAndTypeVar( - header_node_idx, - Content{ .rigid_var = elem_ident }, - Region.zero(), - TypeVar, - ); - const anno_idx = try ir.addTypeAnnoAndTypeVar(.{ .ty = .{ - .symbol = type_ident, - } }, .{ .structure = .{ .box = elem_var } }, Region.zero()); - const anno_var = ModuleEnv.castIdx(TypeAnno.Idx, TypeVar, anno_idx); - - // Create the type declaration statement - const type_decl_stmt = Statement{ - .s_alias_decl = .{ .header = header_idx, .anno = anno_idx }, - }; - - const type_decl_idx = try ir.addStatementAndTypeVar( - type_decl_stmt, - try ir.types.mkAlias( - types.TypeIdent{ .ident_idx = type_ident }, - anno_var, - &.{elem_var}, - ), - Region.zero(), - ); - - // Add to scope without any error checking (built-ins are always valid) - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.put(gpa, .type_decl, type_ident, type_decl_idx); - - try ir.redirectTypeTo(Pattern.Idx, BUILTIN_BOX, ModuleEnv.varFrom(type_decl_idx)); -} - -/// Creates `Bool := [True, False]` -fn addBuiltinTypeBool(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!void { - const gpa = ir.gpa; - const type_ident = try ir.insertIdent(base.Ident.for_text("Bool")); - - // Create a type header for the built-in type - const header_idx = try ir.addTypeHeaderAndTypeVar(.{ - .name = type_ident, - .args = .{ .span = .{ .start = 0, .len = 0 } }, // No type parameters for built-ins - }, .{ .flex_var = null }, Region.zero()); - const header_node_idx = ModuleEnv.nodeIdxFrom(header_idx); - - // Create a type annotation that refers to itself (built-in types are primitive) - const ext_var = try ir.addTypeSlotAndTypeVar( - header_node_idx, - Content{ .structure = .empty_tag_union }, - Region.zero(), - TypeVar, - ); - const anno_idx = try ir.addTypeAnnoAndTypeVar(.{ .ty = .{ - .symbol = type_ident, - } }, try ir.types.mkBool(gpa, ir.getIdentStore(), ext_var), Region.zero()); - const anno_var = ModuleEnv.castIdx(TypeAnno.Idx, TypeVar, anno_idx); - - // Create the type declaration statement - const type_decl_stmt = Statement{ - .s_nominal_decl = .{ .header = header_idx, .anno = anno_idx }, - }; - - const type_decl_idx = try ir.addStatementAndTypeVar( - type_decl_stmt, - try ir.types.mkNominal( - types.TypeIdent{ .ident_idx = type_ident }, - anno_var, - &.{}, - try ir.insertIdent(base.Ident.for_text(ir.module_name)), - ), - Region.zero(), - ); - - // Add to scope without any error checking (built-ins are always valid) - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.put(gpa, .type_decl, type_ident, type_decl_idx); - - try ir.redirectTypeTo(Pattern.Idx, BUILTIN_BOOL, ModuleEnv.varFrom(type_decl_idx)); - - // Add True and False to unqualified_nominal_tags - // TODO: in the future, we should have hardcoded constants for these. - try self.unqualified_nominal_tags.put(gpa, "True", type_decl_idx); - try self.unqualified_nominal_tags.put(gpa, "False", type_decl_idx); + try current_scope.type_bindings.put(gpa, type_ident, Scope.TypeBinding{ + .external_nominal = .{ + .module_ident = builtin_ident, + .original_ident = type_ident, + .target_node_idx = null, + .import_idx = builtin_import_idx, + .origin_region = zero_region, + .module_not_found = false, + }, + }); + } + } } // canonicalize // @@ -550,326 +412,700 @@ const Self = @This(); /// - Eliminates syntax sugar (for example, renaming `+` to the function call `add`). /// /// The canonicalization occurs on a single module (file) in isolation. This allows for this work to be easily parallelized and also cached. So where the source code for a module has not changed, the CanIR can simply be loaded from disk and used immediately. -pub fn canonicalizeFile( +/// First pass helper: Process a type declaration and introduce it into scope +/// If parent_name is provided, creates a qualified name (e.g., "Foo.Bar") +/// relative_parent_name is the parent path without the module prefix (e.g., null for top-level, "Num" for U8 inside Num) +fn processTypeDeclFirstPass( self: *Self, + type_decl: std.meta.fieldInfo(AST.Statement, .type_decl).type, + parent_name: ?Ident.Idx, + relative_parent_name: ?Ident.Idx, + defer_associated_blocks: bool, ) std.mem.Allocator.Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); + // Canonicalize the type declaration header first + const header_idx = try self.canonicalizeTypeHeader(type_decl.header, type_decl.kind); + const region = self.parse_ir.tokenizedRegionToRegion(type_decl.region); - // Assert that everything is in-sync - self.env.debugAssertArraysInSync(); - - const file = self.parse_ir.store.getFile(); - - // canonicalize_header_packages(); - - // First, process the header to create exposed_scope - const header = self.parse_ir.store.getHeader(file.header); - switch (header) { - .module => |h| try self.createExposedScope(h.exposes), - .package => |h| try self.createExposedScope(h.exposes), - .platform => |h| try self.createExposedScope(h.exposes), - .hosted => |h| try self.createExposedScope(h.exposes), - .app => { - // App headers have 'provides' instead of 'exposes' - // TODO: Handle app provides differently - }, - .malformed => { - // Skip malformed headers - }, + // Check if the header is malformed before trying to use it + const node = self.env.store.nodes.get(@enumFromInt(@intFromEnum(header_idx))); + if (node.tag == .malformed) { + // The header is malformed (e.g., because a non-Builtin module tried to declare + // a type with a builtin name). Just return early without processing this type. + return; } - // Track the start of scratch defs and statements - const scratch_defs_start = self.env.store.scratchDefTop(); - const scratch_statements_start = self.env.store.scratch_statements.top(); + // Extract the type name from the header + const type_header = self.env.store.getTypeHeader(header_idx); - // First pass: Process all type declarations to introduce them into scope - for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { - const stmt = self.parse_ir.store.getStatement(stmt_id); - switch (stmt) { - .type_decl => |type_decl| { - // Canonicalize the type declaration header first - const header_idx = try self.canonicalizeTypeHeader(type_decl.header); - const region = self.parse_ir.tokenizedRegionToRegion(type_decl.region); + // Build qualified name and header if we have a parent + const qualified_name_idx = if (parent_name) |parent_idx| blk: { + const parent_text = self.env.getIdent(parent_idx); + const type_text = self.env.getIdent(type_header.name); + break :blk try self.env.insertQualifiedIdent(parent_text, type_text); + } else type_header.name; - // Extract the type name from the header to introduce it into scope early - const type_header = self.env.store.getTypeHeader(header_idx); + // Compute relative_name: the type name without the module prefix + // For nested types like U8 in Num: relative_name = "Num.U8" (relative_parent + type's relative_name) + // For top-level types: relative_name = type_header.relative_name (original unqualified name) + const relative_name_idx: Ident.Idx = if (relative_parent_name) |rel_parent_idx| blk: { + // Nested case: build "Num.U8" from relative_parent="Num" and type="U8" + const rel_parent_text = self.env.getIdent(rel_parent_idx); + const type_relative = self.env.getIdent(type_header.relative_name); + break :blk try self.env.insertQualifiedIdent(rel_parent_text, type_relative); + } else type_header.relative_name; - // Create a placeholder type declaration statement to introduce the type name into scope - // This allows recursive type references to work during annotation canonicalization - const placeholder_cir_type_decl = switch (type_decl.kind) { - .alias => Statement{ - .s_alias_decl = .{ - .header = header_idx, - .anno = @enumFromInt(0), // placeholder - will be replaced - }, + // Create a new header with the qualified name if needed + const final_header_idx = if (parent_name != null and qualified_name_idx.idx != type_header.name.idx) blk: { + const qualified_header = CIR.TypeHeader{ + .name = qualified_name_idx, + .relative_name = relative_name_idx, + .args = type_header.args, + }; + break :blk try self.env.addTypeHeader(qualified_header, region); + } else header_idx; + + // Check if this type was already introduced in Phase 1.5.8 (for forward reference support) + // If so, reuse the existing statement index instead of creating a new one + const type_decl_stmt_idx = if (self.scopeLookupTypeDecl(qualified_name_idx)) |existing_stmt_idx| blk: { + // Type was already introduced - check if it's a placeholder (anno = 0) or a real declaration + const existing_stmt = self.env.store.getStatement(existing_stmt_idx); + const is_placeholder = switch (existing_stmt) { + .s_alias_decl => |alias| alias.anno == .placeholder, + .s_nominal_decl => |nominal| nominal.anno == .placeholder, + else => false, + }; + + if (is_placeholder) { + // It's a placeholder from Phase 1.5.8 - we'll update it + break :blk existing_stmt_idx; + } else { + // It's a real declaration - this is a redeclaration error + // Still create a new statement and report the error + const original_region = self.env.store.getStatementRegion(existing_stmt_idx); + try self.env.pushDiagnostic(Diagnostic{ + .type_redeclared = .{ + .original_region = original_region, + .redeclared_region = region, + .name = qualified_name_idx, + }, + }); + + // Create a new statement for the redeclared type (so both declarations exist in the IR) + const new_stmt = switch (type_decl.kind) { + .alias => Statement{ + .s_alias_decl = .{ + .header = final_header_idx, + .anno = .placeholder, // placeholder, will be overwritten }, - .nominal => Statement{ - .s_nominal_decl = .{ - .header = header_idx, - .anno = @enumFromInt(0), // placeholder - will be replaced - }, + }, + .nominal, .@"opaque" => Statement{ + .s_nominal_decl = .{ + .header = final_header_idx, + .anno = .placeholder, // placeholder, will be overwritten + .is_opaque = type_decl.kind == .@"opaque", + }, + }, + }; + + break :blk try self.env.addStatement(new_stmt, region); + } + } else blk: { + // Type was not introduced yet - create a placeholder statement + const placeholder_cir_type_decl = switch (type_decl.kind) { + .alias => Statement{ + .s_alias_decl = .{ + .header = final_header_idx, + .anno = .placeholder, // placeholder, will be overwritten + }, + }, + .nominal, .@"opaque" => Statement{ + .s_nominal_decl = .{ + .header = final_header_idx, + .anno = .placeholder, // placeholder, will be overwritten + .is_opaque = type_decl.kind == .@"opaque", + }, + }, + }; + + const stmt_idx = try self.env.addStatement(placeholder_cir_type_decl, region); + + // Introduce the type name into scope early to support recursive references + try self.introduceType(qualified_name_idx, stmt_idx, region); + + break :blk stmt_idx; + }; + + // For nested types, also add an unqualified alias so child scopes can find it + // E.g., when introducing "Builtin.Bool", also add "Bool" -> "Builtin.Bool" + // This allows nested scopes (like Str's or Num.U8's associated blocks) to find Bool via scope lookup + if (parent_name != null) { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.introduceTypeAlias(self.env.gpa, type_header.name, type_decl_stmt_idx); + } + + // Process type parameters and annotation in a separate scope + const anno_idx = blk: { + // Enter a new scope for type parameters + const type_var_scope = self.scopeEnterTypeVar(); + defer self.scopeExitTypeVar(type_var_scope); + + // Introduce type parameters from the header into the scope + try self.introduceTypeParametersFromHeader(final_header_idx); + + // Now canonicalize the type annotation with type parameters and type name in scope + break :blk try self.canonicalizeTypeAnno(type_decl.anno, .type_decl_anno); + }; + + // Canonicalize where clauses if present + if (type_decl.where) |_| { + try self.env.pushDiagnostic(Diagnostic{ .where_clause_not_allowed_in_type_decl = .{ + .region = region, + } }); + } + + // Create the real CIR type declaration statement with the canonicalized annotation + const type_decl_stmt = blk: { + switch (type_decl.kind) { + .alias => { + break :blk Statement{ + .s_alias_decl = .{ + .header = final_header_idx, + .anno = anno_idx, }, }; - - const placeholder_type_decl_idx = try self.env.addStatementAndTypeVar(placeholder_cir_type_decl, Content{ .flex_var = null }, region); - - // Introduce the type name into scope early to support recursive references - try self.scopeIntroduceTypeDecl(type_header.name, placeholder_type_decl_idx, region); - - // Process type parameters and annotation in a separate scope - const anno_idx = blk: { - // Enter a new scope for type parameters - try self.scopeEnter(self.env.gpa, false); - defer self.scopeExit(self.env.gpa) catch {}; - - // Introduce type parameters from the header into the scope - try self.introduceTypeParametersFromHeader(header_idx); - - // Now canonicalize the type annotation with type parameters and type name in scope - break :blk try self.canonicalizeTypeAnno(type_decl.anno, .type_decl_anno); + }, + .nominal, .@"opaque" => { + break :blk Statement{ + .s_nominal_decl = .{ + .header = final_header_idx, + .anno = anno_idx, + .is_opaque = type_decl.kind == .@"opaque", + }, }; + }, + } + }; - // Get type variables to args (lhs) - const header_arg_vars: []TypeVar = @ptrCast(self.env.store.sliceTypeAnnos(type_header.args)); + // Create the real statement and add it to scratch statements + try self.env.store.setStatementNode(type_decl_stmt_idx, type_decl_stmt); + try self.env.store.addScratchStatement(type_decl_stmt_idx); - // Get type variable to the backing type (rhs) - const anno_var = ModuleEnv.varFrom(anno_idx); + // For type modules, associate the node index with the exposed type + if (self.env.module_kind == .type_module) { + if (qualified_name_idx == self.env.module_name_idx) { + // This is the main type of the type module - set its node index + const node_idx_u16 = @as(u16, @intCast(@intFromEnum(type_decl_stmt_idx))); + try self.env.setExposedNodeIndexById(qualified_name_idx, node_idx_u16); + } + } - // Check if the backing type is already an error type - const backing_resolved = self.env.types.resolveVar(anno_var); - const backing_is_error = backing_resolved.desc.content == .err; + // Remove from exposed_type_texts since the type is now fully defined + const type_text = self.env.getIdent(type_header.name); + _ = self.exposed_type_texts.remove(type_text); - // The identified of the type - const type_ident = types.TypeIdent{ .ident_idx = type_header.name }; + // Process associated items completely (both symbol introduction and canonicalization) + // This eliminates the need for a separate third pass + // Unless defer_associated_blocks is true (when called from processAssociatedItemsFirstPass + // to handle sibling type forward references) + if (!defer_associated_blocks) { + if (type_decl.associated) |assoc| { + try self.processAssociatedBlock(qualified_name_idx, relative_name_idx, type_header.relative_name, assoc, false); + } + } +} - // Canonicalize where clauses if present - if (type_decl.where) |_| { - try self.env.pushDiagnostic(Diagnostic{ .where_clause_not_allowed_in_type_decl = .{ - .region = region, - } }); +/// Introduce just the type name into scope without processing the full annotation. +/// This is used in Phase 1.5.8 to make type names available for forward references +/// in associated item signatures before the associated blocks are processed. +/// We create a real placeholder statement (with zero annotation) that will be updated in Phase 1.7. +fn introduceTypeNameOnly( + self: *Self, + type_decl: std.meta.fieldInfo(AST.Statement, .type_decl).type, +) std.mem.Allocator.Error!void { + // Canonicalize the type header to get the name in the env's identifier space + const header_idx = try self.canonicalizeTypeHeader(type_decl.header, type_decl.kind); + const region = self.parse_ir.tokenizedRegionToRegion(type_decl.region); + + // Check if the header is malformed + const node = self.env.store.nodes.get(@enumFromInt(@intFromEnum(header_idx))); + if (node.tag == .malformed) { + return; + } + + // Extract the type name from the header + const type_header = self.env.store.getTypeHeader(header_idx); + const name_ident = type_header.name; + + // Check if already introduced (shouldn't happen, but be safe) + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + if (current_scope.type_bindings.get(name_ident) != null) { + return; // Already in scope + } + + // Create a placeholder statement with a zero annotation index + // This will be updated in Phase 1.7 with the real annotation + const placeholder_stmt = switch (type_decl.kind) { + .alias => Statement{ + .s_alias_decl = .{ + .header = header_idx, + .anno = .placeholder, // placeholder, overwritten in Phase 1.7 + }, + }, + .nominal, .@"opaque" => Statement{ + .s_nominal_decl = .{ + .header = header_idx, + .anno = .placeholder, // placeholder, overwritten in Phase 1.7 + .is_opaque = type_decl.kind == .@"opaque", + }, + }, + }; + + const stmt_idx = try self.env.addStatement(placeholder_stmt, region); + + // Introduce the type into scope with the real (placeholder) statement index + try self.introduceType(name_ident, stmt_idx, region); + + // Mark this statement as needing update in Phase 1.7 + // We use exposed_type_texts to track which types need their annotations processed + const type_text = self.env.getIdent(name_ident); + try self.exposed_type_texts.put(self.env.gpa, type_text, region); +} + +/// Recursively introduce nested item aliases into the current scope. +/// Given a type prefix (e.g., "Inner" or "Inner.Deep"), this adds aliases for all items +/// defined in that nested type and all of its nested types. +/// +/// For example, if we're in Outer's scope and Inner has: +/// - Inner.val = 1 +/// - Inner.Deep := [].{ deepVal = 2 } +/// +/// This will add: +/// - "Inner.val" -> pattern for Inner.val +/// - "Inner.Deep.deepVal" -> pattern for Inner.Deep.deepVal +fn introduceNestedItemAliases( + self: *Self, + qualified_parent_idx: Ident.Idx, // Fully qualified parent, e.g., "Module.Outer.Inner" + prefix: []const u8, // User-facing prefix for this scope, e.g., "Inner" + assoc_statements: anytype, +) std.mem.Allocator.Error!void { + for (self.parse_ir.store.statementSlice(assoc_statements)) |assoc_stmt_idx| { + const assoc_stmt = self.parse_ir.store.getStatement(assoc_stmt_idx); + switch (assoc_stmt) { + .decl => |decl| { + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + // Build fully qualified name (e.g., "Module.Outer.Inner.val") + const qualified_text = self.env.getIdent(qualified_parent_idx); + const decl_text = self.env.getIdent(decl_ident); + const full_qualified_ident_idx = try self.env.insertQualifiedIdent(qualified_text, decl_text); + + // Look up the fully qualified pattern + switch (self.scopeLookup(.ident, full_qualified_ident_idx)) { + .found => |pattern_idx| { + const scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Build prefixed name for this scope (e.g., "Inner.val") + // Need to copy prefix to buffer to avoid invalidation + var prefix_buf: [256]u8 = undefined; + if (prefix.len > prefix_buf.len) continue; + @memcpy(prefix_buf[0..prefix.len], prefix); + const safe_prefix = prefix_buf[0..prefix.len]; + + const decl_text_fresh = self.env.getIdent(decl_ident); + const prefixed_ident_idx = try self.env.insertQualifiedIdent(safe_prefix, decl_text_fresh); + try scope.idents.put(self.env.gpa, prefixed_ident_idx, pattern_idx); + }, + .not_found => {}, + } + } + } + }, + .type_decl => |nested_type_decl| { + // Recursively process nested types + if (nested_type_decl.associated) |nested_assoc| { + const nested_header = self.parse_ir.store.getTypeHeader(nested_type_decl.header) catch continue; + const nested_type_ident = self.parse_ir.tokens.resolveIdentifier(nested_header.name) orelse continue; + + // Build fully qualified name for the nested type + const qualified_text = self.env.getIdent(qualified_parent_idx); + const nested_type_text = self.env.getIdent(nested_type_ident); + const nested_qualified_idx = try self.env.insertQualifiedIdent(qualified_text, nested_type_text); + + // Build new prefix (e.g., "Inner.Deep") + // Need to copy to buffer to avoid invalidation + var new_prefix_buf: [256]u8 = undefined; + if (prefix.len > new_prefix_buf.len) continue; + @memcpy(new_prefix_buf[0..prefix.len], prefix); + + // Re-fetch nested_type_text after insertQualifiedIdent may have reallocated + const nested_type_text_fresh = self.env.getIdent(nested_type_ident); + if (prefix.len + 1 + nested_type_text_fresh.len > new_prefix_buf.len) continue; + new_prefix_buf[prefix.len] = '.'; + @memcpy(new_prefix_buf[prefix.len + 1 ..][0..nested_type_text_fresh.len], nested_type_text_fresh); + const new_prefix = new_prefix_buf[0 .. prefix.len + 1 + nested_type_text_fresh.len]; + + // Recursively introduce items from this nested type + try self.introduceNestedItemAliases(nested_qualified_idx, new_prefix, nested_assoc.statements); + } + }, + else => {}, + } + } +} + +/// Process an associated block: introduce all items, set up scope with aliases, and canonicalize +/// When skip_first_pass is true, placeholders were already created by a recursive call to +/// processAssociatedItemsFirstPass, so we skip directly to scope entry and body processing. +/// relative_name is the type's name without module prefix (null for module-level associated blocks) +fn processAssociatedBlock( + self: *Self, + qualified_name_idx: Ident.Idx, + relative_name_idx: ?Ident.Idx, + type_name: Ident.Idx, + assoc: anytype, + skip_first_pass: bool, +) std.mem.Allocator.Error!void { + // First, introduce placeholder patterns for all associated items + // (Skip if this is a nested call where placeholders were already created) + if (!skip_first_pass) { + try self.processAssociatedItemsFirstPass(qualified_name_idx, relative_name_idx, assoc.statements); + } + + // Now enter a new scope for the associated block where both qualified and unqualified names work + try self.scopeEnter(self.env.gpa, false); // false = not a function boundary + defer self.scopeExit(self.env.gpa) catch unreachable; + + // Mark this scope as being for an associated block + { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + current_scope.associated_type_name = type_name; + } + + // Introduce the parent type itself into this scope so it can be referenced by its unqualified name + // For example, if we're processing MyBool's associated items, we need "MyBool" to resolve to "Test.MyBool" + if (self.scopeLookupTypeDecl(qualified_name_idx)) |parent_type_decl_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.introduceTypeAlias(self.env.gpa, type_name, parent_type_decl_idx); + } + + // Note: Sibling types and ancestor types are accessible via parent scope lookup. + // When nested types were introduced in processTypeDeclFirstPass, unqualified aliases + // were added in their declaration scope, making them visible to all child scopes. + + // FIRST: Add decl aliases to current scope BEFORE processing nested blocks. + // This is critical so nested scopes can access parent decls via scope chain lookup. + // For example, in: + // ScopeNested := [OO].{ + // outer = 111 + // Nested := [NN].{ usesOuter = outer } # 'outer' must be visible here + // } + // We need 'outer' in ScopeNested's scope before processing Nested's block. + for (self.parse_ir.store.statementSlice(assoc.statements)) |decl_stmt_idx| { + const decl_stmt = self.parse_ir.store.getStatement(decl_stmt_idx); + if (decl_stmt == .decl) { + const decl = decl_stmt.decl; + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + // Build fully qualified name (e.g., "Test.MyBool.my_not") + const parent_text = self.env.getIdent(qualified_name_idx); + const decl_text = self.env.getIdent(decl_ident); + const fully_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, decl_text); + + // Look up the fully qualified pattern (from module scope via nesting) + switch (self.scopeLookup(.ident, fully_qualified_ident_idx)) { + .found => |pattern_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Add unqualified name (e.g., "my_not") + try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); + + // Add type-qualified name (e.g., "MyBool.my_not") + // Re-fetch strings since insertQualifiedIdent may have reallocated the ident store + const parent_type_text_refetched = self.env.getIdent(type_name); + const decl_text_refetched = self.env.getIdent(decl_ident); + const type_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_type_text_refetched, decl_text_refetched); + try current_scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); + }, + .not_found => {}, + } + } + } + } + } + + // SECOND: Process nested type declarations' associated blocks. + // Now that parent decls are aliased, nested scopes can access them via scope chain lookup. + // We must do this BEFORE we set up aliases for nested items, because those aliases + // need to point to patterns that exist after nested processing completes. + for (self.parse_ir.store.statementSlice(assoc.statements)) |nested_stmt_idx| { + const nested_stmt = self.parse_ir.store.getStatement(nested_stmt_idx); + if (nested_stmt == .type_decl) { + const nested_type_decl = nested_stmt.type_decl; + if (nested_type_decl.associated) |nested_assoc| { + const nested_header = self.parse_ir.store.getTypeHeader(nested_type_decl.header) catch continue; + const nested_type_ident = self.parse_ir.tokens.resolveIdentifier(nested_header.name) orelse continue; + + // Build fully qualified name for the nested type + const parent_text = self.env.getIdent(qualified_name_idx); + const nested_type_text = self.env.getIdent(nested_type_ident); + const nested_qualified_idx = try self.env.insertQualifiedIdent(parent_text, nested_type_text); + + // Build relative name for the nested type (without module prefix) + // If relative_name_idx is not null, use it as the parent (e.g., "Parent.Nested") + // If relative_name_idx is null (module root), just use the nested type's name + const nested_relative_idx = if (relative_name_idx) |rel_idx| blk: { + const rel_parent_text = self.env.getIdent(rel_idx); + break :blk try self.env.insertQualifiedIdent(rel_parent_text, nested_type_text); + } else nested_type_ident; + + // Recursively process the nested type's associated block + // Skip first pass because placeholders were already created by + // processAssociatedItemsFirstPass Phase 2b + try self.processAssociatedBlock(nested_qualified_idx, nested_relative_idx, nested_type_ident, nested_assoc, true); + } + } + } + + // THIRD: Introduce type aliases and nested item aliases into this scope + // We only add unqualified and type-qualified names; fully qualified names are + // already in the parent scope and accessible via scope nesting + for (self.parse_ir.store.statementSlice(assoc.statements)) |assoc_stmt_idx| { + const assoc_stmt = self.parse_ir.store.getStatement(assoc_stmt_idx); + switch (assoc_stmt) { + .type_decl => |nested_type_decl| { + const nested_header = self.parse_ir.store.getTypeHeader(nested_type_decl.header) catch continue; + const unqualified_ident = self.parse_ir.tokens.resolveIdentifier(nested_header.name) orelse continue; + + // Build fully qualified name (e.g., "Test.MyBool") + const parent_text = self.env.getIdent(qualified_name_idx); + const nested_type_text = self.env.getIdent(unqualified_ident); + const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, nested_type_text); + + // Introduce type aliases (fully qualified is already in parent scope from processTypeDeclFirstPass) + if (self.scopeLookupTypeDecl(qualified_ident_idx)) |qualified_type_decl_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Add unqualified alias (e.g., "Bar" -> the fully qualified type) + try current_scope.introduceTypeAlias(self.env.gpa, unqualified_ident, qualified_type_decl_idx); + + // Add user-facing qualified alias (e.g., "Foo.Bar" -> the fully qualified type) + // This allows users to write "Foo.Bar" in type annotations + // Re-fetch nested_type_text since insertQualifiedIdent may have reallocated + const type_name_text_str = self.env.getIdent(type_name); + const nested_type_text_str = self.env.getIdent(unqualified_ident); + const user_qualified_ident_idx = try self.env.insertQualifiedIdent(type_name_text_str, nested_type_text_str); + try current_scope.introduceTypeAlias(self.env.gpa, user_qualified_ident_idx, qualified_type_decl_idx); } - // Create the real CIR type declaration statement with the canonicalized annotation - const real_cir_type_decl, const type_decl_content = blk: { - switch (type_decl.kind) { - .alias => { - const alias_content = if (backing_is_error) - types.Content{ .err = {} } - else - try self.env.types.mkAlias(type_ident, anno_var, header_arg_vars); - - break :blk .{ - Statement{ - .s_alias_decl = .{ - .header = header_idx, - .anno = anno_idx, - }, - }, - alias_content, - }; - }, - .nominal => { - const nominal_content = if (backing_is_error) - types.Content{ .err = {} } - else - try self.env.types.mkNominal( - type_ident, - anno_var, - header_arg_vars, - try self.env.insertIdent(base.Ident.for_text(self.env.module_name)), - ); - - break :blk .{ - Statement{ - .s_nominal_decl = .{ - .header = header_idx, - .anno = anno_idx, - }, - }, - nominal_content, - }; - }, - } - }; - - // Create the real statement and add it to scratch statements - const type_decl_stmt_idx = try self.env.addStatementAndTypeVar(real_cir_type_decl, type_decl_content, region); - try self.env.store.addScratchStatement(type_decl_stmt_idx); - - // Update the scope to point to the real statement instead of the placeholder - try self.scopeUpdateTypeDecl(type_header.name, type_decl_stmt_idx); - - // Remove from exposed_type_texts since the type is now fully defined - const type_text = self.env.getIdent(type_header.name); - _ = self.exposed_type_texts.remove(type_text); + // Introduce associated items of nested types into this scope (recursively) + // Now that nested blocks have been processed, these patterns exist and can be aliased. + // This allows qualified access like "Inner.val" and "Inner.Deep.deepVal" from the parent scope. + if (nested_type_decl.associated) |nested_assoc| { + try self.introduceNestedItemAliases(qualified_ident_idx, nested_type_text, nested_assoc.statements); + } + }, + .decl => { + // Decl aliases were already added in the FIRST pass above + // (before processing nested blocks, so nested scopes can see parent decls) }, else => { - // Skip non-type-declaration statements in first pass + // Note: .type_anno is not handled here because anno-only patterns + // are created during processAssociatedItemsSecondPass, so they need + // to be re-introduced AFTER that call completes }, } } - // Second pass: Process all other statements - var last_type_anno: ?struct { - name: base.Ident.Idx, - anno_idx: TypeAnno.Idx, - type_vars: DataSpan, - where_clauses: ?WhereClause.Span, - } = null; + // Process the associated items (canonicalize their bodies) + try self.processAssociatedItemsSecondPass(qualified_name_idx, type_name, assoc.statements); - for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { - const stmt = self.parse_ir.store.getStatement(stmt_id); - switch (stmt) { - .import => |import_stmt| { - _ = try self.canonicalizeImportStatement(import_stmt); - last_type_anno = null; // Clear on non-annotation statement - }, - .decl => |decl| { - // Check if this declaration matches the last type annotation - var annotation_idx: ?Annotation.Idx = null; - if (last_type_anno) |anno_info| { - if (self.parse_ir.store.getPattern(decl.pattern) == .ident) { - const pattern_ident = self.parse_ir.store.getPattern(decl.pattern).ident; - if (self.parse_ir.tokens.resolveIdentifier(pattern_ident.ident_tok)) |decl_ident| { - if (anno_info.name.idx == decl_ident.idx) { - // This declaration matches the type annotation - const pattern_region = self.parse_ir.tokenizedRegionToRegion(self.parse_ir.store.getPattern(decl.pattern).to_tokenized_region()); - annotation_idx = try self.createAnnotationFromTypeAnno(anno_info.anno_idx, pattern_region); - // Clear the annotation since we've used it - last_type_anno = null; - } - } + // After processing, introduce anno-only defs into the associated block scope + // (They were just created by processAssociatedItemsSecondPass) + // We only add unqualified and type-qualified names; fully qualified is in parent scope + for (self.parse_ir.store.statementSlice(assoc.statements)) |anno_stmt_idx| { + const anno_stmt = self.parse_ir.store.getStatement(anno_stmt_idx); + switch (anno_stmt) { + .type_anno => |type_anno| { + if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { + // Build fully qualified name (e.g., "Test.MyBool.len") + const parent_text = self.env.getIdent(qualified_name_idx); + const anno_text = self.env.getIdent(anno_ident); + const fully_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); + + // Look up the fully qualified pattern (from parent scope via nesting) + switch (self.scopeLookup(.ident, fully_qualified_ident_idx)) { + .found => |pattern_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Add unqualified name (e.g., "len") + try current_scope.idents.put(self.env.gpa, anno_ident, pattern_idx); + + // Add type-qualified name (e.g., "List.len") + // Re-fetch strings since insertQualifiedIdent may have reallocated the ident store + const parent_type_text_refetched = self.env.getIdent(type_name); + const anno_text_refetched = self.env.getIdent(anno_ident); + const type_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_type_text_refetched, anno_text_refetched); + try current_scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); + }, + .not_found => { + // This can happen if the type_anno was followed by a matching decl + // (in which case it's not an anno-only def) + }, } } - - const def_idx = try self.canonicalizeDeclWithAnnotation(decl, annotation_idx); - try self.env.store.addScratchDef(def_idx); - last_type_anno = null; // Clear after successful use - - // If this declaration successfully defined an exposed value, remove it from exposed_ident_texts - // and add the node index to exposed_items - const pattern = self.parse_ir.store.getPattern(decl.pattern); - if (pattern == .ident) { - const token_region = self.parse_ir.tokens.resolve(@intCast(pattern.ident.ident_tok)); - const ident_text = self.parse_ir.env.source[token_region.start.offset..token_region.end.offset]; - - // If this identifier is exposed, add it to exposed_items - if (self.exposed_ident_texts.contains(ident_text)) { - // Get the interned identifier - it should already exist from parsing - const ident = base.Ident.for_text(ident_text); - const idx = try self.env.insertIdent(ident); - // Store the def index as u16 in exposed_items - const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); - try self.env.setExposedNodeIndexById(idx, def_idx_u16); - } - - _ = self.exposed_ident_texts.remove(ident_text); - } }, - .@"var" => |var_stmt| { - // Not valid at top-level - const string_idx = try self.env.insertString("var"); - const region = self.parse_ir.tokenizedRegionToRegion(var_stmt.region); - try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ - .stmt = string_idx, - .region = region, - } }); - last_type_anno = null; // Clear on non-annotation statement - }, - .expr => |expr_stmt| { - // Not valid at top-level - const string_idx = try self.env.insertString("expression"); - const region = self.parse_ir.tokenizedRegionToRegion(expr_stmt.region); - try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ - .stmt = string_idx, - .region = region, - } }); - last_type_anno = null; // Clear on non-annotation statement - }, - .crash => |crash_stmt| { - // Not valid at top-level - const string_idx = try self.env.insertString("crash"); - const region = self.parse_ir.tokenizedRegionToRegion(crash_stmt.region); - try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ - .stmt = string_idx, - .region = region, - } }); - last_type_anno = null; // Clear on non-annotation statement - }, - .dbg => |dbg_stmt| { - // Not valid at top-level - const string_idx = try self.env.insertString("dbg"); - const region = self.parse_ir.tokenizedRegionToRegion(dbg_stmt.region); - try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ - .stmt = string_idx, - .region = region, - } }); - last_type_anno = null; // Clear on non-annotation statement - }, - .expect => |e| { - // Top-level expect statement - const region = self.parse_ir.tokenizedRegionToRegion(e.region); + else => {}, + } + } +} - // Canonicalize the expect expression - const can_expect = try self.canonicalizeExpr(e.body) orelse { - // If canonicalization fails, create a malformed expression - const malformed = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ - .region = region, - } }); - const expect_stmt = Statement{ .s_expect = .{ - .body = malformed, - } }; - const expect_stmt_idx = try self.env.addStatementAndTypeVar(expect_stmt, Content{ .flex_var = null }, region); - try self.env.store.addScratchStatement(expect_stmt_idx); - last_type_anno = null; // Clear on non-annotation statement - continue; +/// Canonicalize an associated item declaration with a qualified name +fn canonicalizeAssociatedDecl( + self: *Self, + decl: AST.Statement.Decl, + qualified_ident: Ident.Idx, +) std.mem.Allocator.Error!CIR.Def.Idx { + const trace = tracy.trace(@src()); + defer trace.end(); + + const pattern_region = self.parse_ir.tokenizedRegionToRegion(self.parse_ir.store.getPattern(decl.pattern).to_tokenized_region()); + + // Create or upgrade the pattern for this declaration + // Unlike the old two-pass system, we create the pattern on demand now + const pattern_idx = blk: { + const lookup_result = self.scopeLookup(.ident, qualified_ident); + switch (lookup_result) { + .found => |pattern| break :blk pattern, + .not_found => { + // Pattern doesn't exist yet - create it now + const ident_pattern = Pattern{ + .assign = .{ .ident = qualified_ident }, }; + const new_pattern_idx = try self.env.addPattern(ident_pattern, pattern_region); - // Create expect statement - const expect_stmt = Statement{ .s_expect = .{ - .body = can_expect.idx, - } }; - const expect_stmt_idx = try self.env.addStatementAndTypeVar(expect_stmt, Content{ .flex_var = null }, region); - try self.env.store.addScratchStatement(expect_stmt_idx); + // Introduce it into BOTH the current scope (for sibling references) + // and the parent scope (for external references after scope exit) + _ = try self.scopeIntroduceInternal(self.env.gpa, .ident, qualified_ident, new_pattern_idx, false, true); - last_type_anno = null; // Clear on non-annotation statement + // Also introduce into parent scope so it persists after associated block scope exits + if (self.scopes.items.len >= 2) { + const parent_scope = &self.scopes.items[self.scopes.items.len - 2]; + try parent_scope.idents.put(self.env.gpa, qualified_ident, new_pattern_idx); + } + + break :blk new_pattern_idx; }, - .@"for" => |for_stmt| { - // Not valid at top-level - const string_idx = try self.env.insertString("for"); - const region = self.parse_ir.tokenizedRegionToRegion(for_stmt.region); - try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ - .stmt = string_idx, - .region = region, - } }); - }, - .@"return" => |return_stmt| { - // Not valid at top-level - const string_idx = try self.env.insertString("return"); - const region = self.parse_ir.tokenizedRegionToRegion(return_stmt.region); - try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ - .stmt = string_idx, - .region = region, - } }); + } + }; + + // Canonicalize the body expression in expression context (RHS of assignment) + const saved_stmt_pos = self.in_statement_position; + self.in_statement_position = false; + const can_expr = try self.canonicalizeExprOrMalformed(decl.body); + self.in_statement_position = saved_stmt_pos; + + // Create the def with no annotation (type annotations are handled via canonicalizeAssociatedDeclWithAnno) + const def = CIR.Def{ + .pattern = pattern_idx, + .expr = can_expr.idx, + .annotation = null, + .kind = .{ .let = {} }, + }; + + const def_idx = try self.env.addDef(def, pattern_region); + return def_idx; +} + +/// Canonicalize an associated item declaration with a type annotation +fn canonicalizeAssociatedDeclWithAnno( + self: *Self, + decl: AST.Statement.Decl, + qualified_ident: Ident.Idx, + type_anno_idx: CIR.TypeAnno.Idx, + mb_where_clauses: ?CIR.WhereClause.Span, +) std.mem.Allocator.Error!CIR.Def.Idx { + const trace = tracy.trace(@src()); + defer trace.end(); + + const pattern_region = self.parse_ir.tokenizedRegionToRegion(self.parse_ir.store.getPattern(decl.pattern).to_tokenized_region()); + + // Create or upgrade the pattern for this declaration + // Unlike the old two-pass system, we create the pattern on demand now + const pattern_idx = blk: { + const lookup_result = self.scopeLookup(.ident, qualified_ident); + switch (lookup_result) { + .found => |pattern| break :blk pattern, + .not_found => { + // Pattern doesn't exist yet - create it now + const ident_pattern = Pattern{ + .assign = .{ .ident = qualified_ident }, + }; + const new_pattern_idx = try self.env.addPattern(ident_pattern, pattern_region); + + // Introduce it into BOTH the current scope (for sibling references) + // and the parent scope (for external references after scope exit) + _ = try self.scopeIntroduceInternal(self.env.gpa, .ident, qualified_ident, new_pattern_idx, false, true); + + // Also introduce into parent scope so it persists after associated block scope exits + if (self.scopes.items.len >= 2) { + const parent_scope = &self.scopes.items[self.scopes.items.len - 2]; + try parent_scope.idents.put(self.env.gpa, qualified_ident, new_pattern_idx); + } + + break :blk new_pattern_idx; }, + } + }; + + // Canonicalize the body expression in expression context (RHS of assignment) + const saved_stmt_pos = self.in_statement_position; + self.in_statement_position = false; + const can_expr = try self.canonicalizeExprOrMalformed(decl.body); + self.in_statement_position = saved_stmt_pos; + + // Create the annotation structure + const annotation = CIR.Annotation{ + .anno = type_anno_idx, + .where = mb_where_clauses, + }; + const annotation_idx = try self.env.addAnnotation(annotation, pattern_region); + + // Create the def with the type annotation + const def = CIR.Def{ + .pattern = pattern_idx, + .expr = can_expr.idx, + .annotation = annotation_idx, + .kind = .{ .let = {} }, + }; + + const def_idx = try self.env.addDef(def, pattern_region); + return def_idx; +} + +/// Second pass helper: Canonicalize associated item definitions +fn processAssociatedItemsSecondPass( + self: *Self, + parent_name: Ident.Idx, + type_name: Ident.Idx, + statements: AST.Statement.Span, +) std.mem.Allocator.Error!void { + const stmt_idxs = self.parse_ir.store.statementSlice(statements); + var i: usize = 0; + while (i < stmt_idxs.len) : (i += 1) { + const stmt_idx = stmt_idxs[i]; + const stmt = self.parse_ir.store.getStatement(stmt_idx); + switch (stmt) { .type_decl => { - // Already processed in first pass, skip - last_type_anno = null; // Clear on non-annotation statement + // Skip nested type declarations - they're already processed by processAssociatedItemsFirstPass Phase 2 + // which calls processAssociatedBlock for each nested type }, .type_anno => |ta| { - const region = self.parse_ir.tokenizedRegionToRegion(ta.region); - - // Top-level type annotation - store for connection to next declaration const name_ident = self.parse_ir.tokens.resolveIdentifier(ta.name) orelse { // Malformed identifier - skip this annotation - const feature = try self.env.insertString("handle malformed identifier for a type annotation"); - try self.env.pushDiagnostic(Diagnostic{ - .not_implemented = .{ - .feature = feature, - .region = region, - }, - }); continue; }; @@ -879,9 +1115,10 @@ pub fn canonicalizeFile( // Extract type variables from the AST annotation try self.extractTypeVarIdentsFromASTAnno(ta.anno, type_vars_top); - // Enter a new scope for type variables - try self.scopeEnter(self.env.gpa, false); - defer self.scopeExit(self.env.gpa) catch {}; + // Enter a new type var scope + const type_var_scope = self.scopeEnterTypeVar(); + defer self.scopeExitTypeVar(type_var_scope); + std.debug.assert(type_var_scope.idx == 0); // Now canonicalize the annotation with type variables in scope const type_anno_idx = try self.canonicalizeTypeAnno(ta.anno, .inline_anno); @@ -899,31 +1136,1152 @@ pub fn canonicalizeFile( break :blk try self.env.store.whereClauseSpanFrom(where_start); } else null; - // If we have where clauses, create a separate s_type_anno statement - if (where_clauses != null) { - const type_anno_stmt = Statement{ - .s_type_anno = .{ - .name = name_ident, - .anno = type_anno_idx, - .where = where_clauses, + // Now, check the next stmt to see if it matches this anno + const next_i = i + 1; + const has_matching_decl = if (next_i < stmt_idxs.len) blk: { + const next_stmt_id = stmt_idxs[next_i]; + const next_stmt = self.parse_ir.store.getStatement(next_stmt_id); + + if (next_stmt == .decl) { + const decl = next_stmt.decl; + // Check if the declaration pattern matches the annotation name + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + // Check if names match + if (name_ident.idx == decl_ident.idx) { + // Skip the next statement since we're processing it now + i = next_i; + + // Build qualified name (e.g., "Foo.bar") + const parent_text = self.env.getIdent(parent_name); + const decl_text = self.env.getIdent(decl_ident); + const qualified_idx = try self.env.insertQualifiedIdent(parent_text, decl_text); + + // Canonicalize with the qualified name and type annotation + const def_idx = try self.canonicalizeAssociatedDeclWithAnno( + decl, + qualified_idx, + type_anno_idx, + where_clauses, + ); + try self.env.store.addScratchDef(def_idx); + + // Register this associated item by its qualified name + const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); + try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); + + // Register the method ident mapping for fast index-based lookup + try self.env.registerMethodIdent(type_name, decl_ident, qualified_idx); + + // Add aliases for this item in the current (associated block) scope + const def_cir = self.env.store.getDef(def_idx); + const pattern_idx = def_cir.pattern; + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Add unqualified name (e.g., "bar") to current scope only + try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); + + // Add type-qualified name (e.g., "Foo.bar") to the scope where the type is defined and ALL ancestor scopes + const type_text = self.env.getIdent(type_name); + const type_qualified_ident_idx = try self.env.insertQualifiedIdent(type_text, decl_text); + + // Find the scope where the parent type is defined (linear search backward) + var type_home_scope_idx: usize = 0; // Default to module scope if not found + var search_idx = self.scopes.items.len; + while (search_idx > 0) { + search_idx -= 1; + if (self.scopes.items[search_idx].idents.get(type_name)) |_| { + type_home_scope_idx = search_idx; + break; + } + } + + // Add type-qualified name to the type's home scope and all ancestors + var scope_idx = type_home_scope_idx; + while (true) { + try self.scopes.items[scope_idx].idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); + if (scope_idx == 0) break; + scope_idx -= 1; + } + + break :blk true; // Found and processed matching decl + } + } + } + } + break :blk false; // No matching decl found + } else false; // No next statement + + // If there's no matching decl, create an anno-only def + if (!has_matching_decl) { + const region = self.parse_ir.tokenizedRegionToRegion(ta.region); + + // Build qualified name for the annotation (e.g., "Str.isEmpty") + const parent_text = self.env.getIdent(parent_name); + const name_text = self.env.getIdent(name_ident); + const qualified_idx = try self.env.insertQualifiedIdent(parent_text, name_text); + // Create anno-only def with the qualified name + const def_idx = try self.createAnnoOnlyDef(qualified_idx, type_anno_idx, where_clauses, region); + + // Register this associated item by its qualified name + const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); + try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); + + // Register the method ident mapping for fast index-based lookup + try self.env.registerMethodIdent(type_name, name_ident, qualified_idx); + + // Pattern is now available in scope (was created in createAnnoOnlyDef) + + try self.env.store.addScratchDef(def_idx); + } + }, + .decl => |decl| { + // Canonicalize the declaration with qualified name + const pattern = self.parse_ir.store.getPattern(decl.pattern); + const pattern_region = self.parse_ir.tokenizedRegionToRegion(pattern.to_tokenized_region()); + if (pattern == .ident) { + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + // Build qualified name (e.g., "Foo.bar") + const parent_text = self.env.getIdent(parent_name); + const decl_text = self.env.getIdent(decl_ident); + const qualified_idx = try self.env.insertQualifiedIdent(parent_text, decl_text); + + // Canonicalize with the qualified name + const def_idx = try self.canonicalizeAssociatedDecl(decl, qualified_idx); + try self.env.store.addScratchDef(def_idx); + + // Register this associated item by its qualified name + const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); + try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); + + // Register the method ident mapping for fast index-based lookup + try self.env.registerMethodIdent(type_name, decl_ident, qualified_idx); + + // Add aliases for this item in the current (associated block) scope + // so it can be referenced by unqualified and type-qualified names + const def_cir = self.env.store.getDef(def_idx); + const pattern_idx = def_cir.pattern; + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Add unqualified name (e.g., "bar") to current scope only + try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); + + // Add type-qualified name (e.g., "Foo.bar") to the scope where the type is defined and ALL ancestor scopes + const type_text = self.env.getIdent(type_name); + const type_qualified_ident_idx = try self.env.insertQualifiedIdent(type_text, decl_text); + + // Find the scope where the parent type is defined (linear search backward) + var type_home_scope_idx: usize = 0; // Default to module scope if not found + var search_idx = self.scopes.items.len; + while (search_idx > 0) { + search_idx -= 1; + if (self.scopes.items[search_idx].idents.get(type_name)) |_| { + type_home_scope_idx = search_idx; + break; + } + } + + // Add type-qualified name to the type's home scope and all ancestors + var scope_idx = type_home_scope_idx; + while (true) { + try self.scopes.items[scope_idx].idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); + if (scope_idx == 0) break; + scope_idx -= 1; + } + } + } else { + // Non-identifier patterns are not supported in associated blocks + const feature = try self.env.insertString("non-identifier patterns in associated blocks"); + try self.env.pushDiagnostic(Diagnostic{ + .not_implemented = .{ + .feature = feature, + .region = pattern_region, + }, + }); + } + }, + .import => { + // Imports are not valid in associated blocks + const region = self.parse_ir.tokenizedRegionToRegion(stmt.import.region); + const feature = try self.env.insertString("import statements in associated blocks"); + try self.env.pushDiagnostic(Diagnostic{ + .not_implemented = .{ + .feature = feature, + .region = region, + }, + }); + }, + else => { + // Other statement types (var, expr, crash, dbg, expect, for, return, malformed) + // are not valid in associated blocks but are already caught by the parser, + // so we don't need to emit additional diagnostics here + }, + } + } +} + +/// Register the user-facing fully qualified name in the module scope. +/// Given a fully qualified name like "module.Foo.Bar.baz", this registers: +/// - "Foo.Bar.baz" in module scope (user-facing fully qualified) +/// +/// Note: Partially qualified names (e.g., "Bar.baz" for Foo's scope, "baz" for Bar's scope) +/// are NOT registered here. They are registered in processAssociatedBlock when the +/// actual scopes are entered, via the alias registration logic there. +fn registerUserFacingName( + self: *Self, + fully_qualified_idx: Ident.Idx, + pattern_idx: CIR.Pattern.Idx, +) std.mem.Allocator.Error!void { + + // Get the fully qualified text and strip the module prefix + const fully_qualified_text = self.env.getIdent(fully_qualified_idx); + const module_prefix = self.env.module_name; + + // Must start with module prefix + if (!std.mem.startsWith(u8, fully_qualified_text, module_prefix) or + fully_qualified_text.len <= module_prefix.len or + fully_qualified_text[module_prefix.len] != '.') + { + return; // Not a module-qualified identifier, nothing to register + } + + // Get user-facing text (without module prefix), e.g., "Foo.Bar.baz" + const user_facing_start = module_prefix.len + 1; + const user_facing_text = fully_qualified_text[user_facing_start..]; + + // Copy to local buffer to avoid invalidation during insertIdent calls + var buf: [512]u8 = undefined; + if (user_facing_text.len > buf.len) return; + @memcpy(buf[0..user_facing_text.len], user_facing_text); + const user_text = buf[0..user_facing_text.len]; + + // Register ONLY the fully qualified user-facing name in the module scope. + // For "Foo.Bar.baz", we register just "Foo.Bar.baz" - not the shorter suffixes. + const user_facing_idx = try self.env.insertIdent(base.Ident.for_text(user_text)); + try self.scopes.items[0].idents.put(self.env.gpa, user_facing_idx, pattern_idx); +} + +/// Register an identifier hierarchically into all active scopes using scope hierarchy information. +/// Given parent_qualified_idx (e.g., "Module.Foo.Bar") and item_name_idx (e.g., "baz"): +/// - Walks up the scope stack, collecting associated_type_name from each scope +/// - Builds qualified names by combining type hierarchy components with the item name +/// - Registers appropriate partially-qualified names in each scope +/// +/// For example, with "Module.Foo.Bar" and "baz": +/// - Module scope: "Module.Foo.Bar.baz" +/// - Foo scope: "Foo.Bar.baz", "Bar.baz" +/// - Bar scope: "baz" +fn registerIdentifierHierarchically( + self: *Self, + _: Ident.Idx, // parent_qualified_idx - unused (TODO: remove this function entirely) + item_name_idx: Ident.Idx, + pattern_idx: CIR.Pattern.Idx, +) std.mem.Allocator.Error!void { + // Build list of type name components from the scope hierarchy + // Walk backwards through scopes, collecting associated_type_name values + var type_components = std.ArrayList(Ident.Idx).init(self.env.gpa); + defer type_components.deinit(); + + // Collect type components from scope hierarchy (in reverse order, from innermost to outermost) + var scope_idx: usize = self.scopes.items.len; + while (scope_idx > 0) { + scope_idx -= 1; + const scope = &self.scopes.items[scope_idx]; + if (scope.associated_type_name) |type_name| { + try type_components.append(type_name); + } + } + + // Reverse to get outermost-to-innermost order + std.mem.reverse(Ident.Idx, type_components.items); + + // Now register into each scope + // The pattern: scope at depth D (from root) gets names starting from component D + const num_scopes = self.scopes.items.len; + const num_type_components = type_components.items.len; + + var current_scope_idx: usize = 0; + while (current_scope_idx < num_scopes) : (current_scope_idx += 1) { + const scope = &self.scopes.items[current_scope_idx]; + + // How many type scopes deep are we from the end? + const depth_from_end = num_type_components - @as(usize, if (current_scope_idx < num_scopes - num_type_components) 0 else (current_scope_idx - (num_scopes - num_type_components - 1))); + + // In the innermost type scope, register only unqualified name + // In outer scopes, register progressively more qualified names + if (depth_from_end == 0 and scope.associated_type_name != null) { + // Innermost scope - just the item name + try scope.idents.put(self.env.gpa, item_name_idx, pattern_idx); + } else if (scope.associated_type_name != null or current_scope_idx == 0) { + // Register partially qualified names + // Start from the current scope's position in the type hierarchy + const start_component = if (scope.associated_type_name != null) blk: { + // Find which component this scope corresponds to + var comp_idx: usize = 0; + while (comp_idx < type_components.items.len) : (comp_idx += 1) { + if (type_components.items[comp_idx].eql(scope.associated_type_name.?)) { + break :blk comp_idx; + } + } + break :blk 0; + } else 0; + + // Register all suffixes from this scope's level down to item level + var comp_skip: usize = start_component; + while (comp_skip < type_components.items.len) : (comp_skip += 1) { + // Build qualified name from component[comp_skip..] + item_name + var current_qualified = type_components.items[comp_skip]; + var comp_build = comp_skip + 1; + while (comp_build < type_components.items.len) : (comp_build += 1) { + const comp_text = self.env.getIdent(current_qualified); + const next_comp_text = self.env.getIdent(type_components.items[comp_build]); + current_qualified = try self.env.insertQualifiedIdent(comp_text, next_comp_text); + } + // Add item name + const final_text = self.env.getIdent(current_qualified); + const item_text = self.env.getIdent(item_name_idx); + const final_qualified = try self.env.insertQualifiedIdent(final_text, item_text); + try scope.idents.put(self.env.gpa, final_qualified, pattern_idx); + } + } + } +} + +/// Update a hierarchically-registered placeholder in all scopes. +/// Updates all suffixes that were registered hierarchically. +fn updatePlaceholderHierarchically( + self: *Self, + fully_qualified_idx: Ident.Idx, + new_pattern_idx: CIR.Pattern.Idx, +) std.mem.Allocator.Error!void { + const fully_qualified_text = self.env.getIdent(fully_qualified_idx); + + // Count components + var component_count: usize = 1; + for (fully_qualified_text) |c| { + if (c == '.') component_count += 1; + } + + // Update all suffixes in each scope using the same logic as registration + const num_scopes = self.scopes.items.len; + var scope_idx: usize = 0; + while (scope_idx < num_scopes) : (scope_idx += 1) { + const scope = &self.scopes.items[scope_idx]; + + const depth_from_end = num_scopes - 1 - scope_idx; + + const min_skip = scope_idx; + const max_skip = if (component_count > depth_from_end + 1) + component_count - depth_from_end - 1 + else + component_count - 1; + + var components_to_skip = min_skip; + while (components_to_skip <= max_skip and components_to_skip < component_count) : (components_to_skip += 1) { + const name_for_this_suffix = if (components_to_skip == 0) + fully_qualified_text + else blk: { + var dots_seen: usize = 0; + var start_pos: usize = 0; + for (fully_qualified_text, 0..) |c, i| { + if (c == '.') { + dots_seen += 1; + if (dots_seen == components_to_skip) { + start_pos = i + 1; + break; + } + } + } + break :blk fully_qualified_text[start_pos..]; + }; + + const ident_for_this_suffix = try self.env.insertIdent(base.Ident.for_text(name_for_this_suffix)); + if (scope.idents.get(ident_for_this_suffix)) |_| { + try scope.idents.put(self.env.gpa, ident_for_this_suffix, new_pattern_idx); + } + } + } + + // Remove from placeholder tracking + _ = self.placeholder_idents.remove(fully_qualified_idx); +} + +/// First pass helper: Process associated items and introduce them into scope with qualified names +/// relative_parent_name is the parent path without the module prefix (null for top-level in module) +fn processAssociatedItemsFirstPass( + self: *Self, + parent_name: Ident.Idx, + relative_parent_name: ?Ident.Idx, + statements: AST.Statement.Span, +) std.mem.Allocator.Error!void { + // Multi-phase approach for sibling types: + // Phase 1a: Introduce nominal type declarations (defer annotations and associated blocks) + // Phase 1b: Add user-facing aliases for the nominal types + // Phase 1c: Process type aliases (which can now reference the nominal types) + // Phase 2: Process deferred associated blocks + + // Phase 1a: Introduce nominal type declarations WITHOUT processing annotations/associated blocks + // This creates placeholder types that can be referenced + for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { + const stmt = self.parse_ir.store.getStatement(stmt_idx); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + // Only process nominal/opaque types in this phase; aliases will be processed later + if (type_decl.kind == .nominal or type_decl.kind == .@"opaque") { + try self.processTypeDeclFirstPass(type_decl, parent_name, relative_parent_name, true); // defer associated blocks + } + } + } + + // Phase 1b: Add user-facing qualified aliases for nominal types + // This must happen before Phase 1c so that type aliases can reference these types + for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { + const stmt = self.parse_ir.store.getStatement(stmt_idx); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + if (type_decl.kind == .nominal or type_decl.kind == .@"opaque") { + const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; + const nested_type_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; + + // Build fully qualified name (e.g., "module.Foo.Bar") + const parent_text = self.env.getIdent(parent_name); + const nested_type_text = self.env.getIdent(nested_type_ident); + const fully_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, nested_type_text); + + // Look up the fully qualified type that was just registered + if (self.scopeLookupTypeDecl(fully_qualified_ident_idx)) |type_decl_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Build user-facing qualified name by stripping module prefix + // Re-fetch strings since insertQualifiedIdent may have reallocated + const fully_qualified_text = self.env.getIdent(fully_qualified_ident_idx); + const module_prefix = self.env.module_name; + + // Check if the fully qualified name starts with the module name + const user_facing_text = if (std.mem.startsWith(u8, fully_qualified_text, module_prefix) and + fully_qualified_text.len > module_prefix.len and + fully_qualified_text[module_prefix.len] == '.') + fully_qualified_text[module_prefix.len + 1 ..] // Skip "module." + else + fully_qualified_text; // No module prefix, use as-is + + // Only add alias if it's different from the fully qualified name + if (!std.mem.eql(u8, user_facing_text, fully_qualified_text)) { + const user_qualified_ident_idx = try self.env.insertIdent(base.Ident.for_text(user_facing_text)); + try current_scope.introduceTypeAlias(self.env.gpa, user_qualified_ident_idx, type_decl_idx); + } + } + } + } + } + + // Phase 1c: Now process type aliases (which can reference the nominal types registered above) + for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { + const stmt = self.parse_ir.store.getStatement(stmt_idx); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + if (type_decl.kind == .alias) { + try self.processTypeDeclFirstPass(type_decl, parent_name, relative_parent_name, true); // defer associated blocks + } + } + } + + // Phase 1d: Add user-facing aliases for type aliases too + for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { + const stmt = self.parse_ir.store.getStatement(stmt_idx); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + if (type_decl.kind == .alias) { + const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; + const nested_type_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; + + // Build fully qualified name + const parent_text = self.env.getIdent(parent_name); + const nested_type_text = self.env.getIdent(nested_type_ident); + const fully_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, nested_type_text); + + // Look up the type alias + if (self.scopeLookupTypeDecl(fully_qualified_ident_idx)) |type_decl_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Build user-facing qualified name by stripping module prefix + const fully_qualified_text = self.env.getIdent(fully_qualified_ident_idx); + const module_prefix = self.env.module_name; + + const user_facing_text = if (std.mem.startsWith(u8, fully_qualified_text, module_prefix) and + fully_qualified_text.len > module_prefix.len and + fully_qualified_text[module_prefix.len] == '.') + fully_qualified_text[module_prefix.len + 1 ..] + else + fully_qualified_text; + + if (!std.mem.eql(u8, user_facing_text, fully_qualified_text)) { + const user_qualified_ident_idx = try self.env.insertIdent(base.Ident.for_text(user_facing_text)); + try current_scope.introduceTypeAlias(self.env.gpa, user_qualified_ident_idx, type_decl_idx); + } + } + } + } + } + + // Phase 2a: Introduce all value declarations and type annotations first (before processing associated blocks) + // This ensures that sibling items (like list_get_unsafe) are available + // when processing associated blocks (like List's) + for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { + const stmt = self.parse_ir.store.getStatement(stmt_idx); + switch (stmt) { + .decl => |decl| { + // Create placeholder for declarations so they can be referenced by sibling types + // processAssociatedItemsSecondPass will later use updatePlaceholder to replace these + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const pattern_region = self.parse_ir.tokenizedRegionToRegion(pattern.to_tokenized_region()); + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + // Build qualified name (e.g., "module.Foo.Bar.baz") + const qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_name), self.env.getIdent(decl_ident)); + + // Create placeholder pattern with qualified name + const placeholder_pattern = Pattern{ + .assign = .{ + .ident = qualified_idx, + }, + }; + const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, pattern_region); + + // Register in parent scope only, tracking component parts + try self.placeholder_idents.put(self.env.gpa, qualified_idx, .{ + .parent_qualified_idx = parent_name, + .item_name_idx = decl_ident, + }); + + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.idents.put(self.env.gpa, qualified_idx, placeholder_pattern_idx); + + // Register progressively qualified names at each scope level per the plan: + // - Module scope gets "Foo.Bar.baz" (user-facing fully qualified) + // - Foo's scope gets "Bar.baz" (partially qualified) + // - Bar's scope gets "baz" (unqualified) + try self.registerUserFacingName(qualified_idx, placeholder_pattern_idx); + } + } + }, + .type_anno => |type_anno| { + // Create placeholder for anno-only defs so they can be referenced by sibling types + // processAssociatedItemsSecondPass will later use updatePlaceholder to replace these + if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { + const qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_name), self.env.getIdent(anno_ident)); + + const region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); + const placeholder_pattern = Pattern{ + .assign = .{ + .ident = qualified_idx, }, }; - const type_anno_stmt_idx = try self.env.addStatementAndTypeVar(type_anno_stmt, Content{ .flex_var = null }, region); - try self.env.store.addScratchStatement(type_anno_stmt_idx); + const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); + + // Register in parent scope only, tracking component parts + try self.placeholder_idents.put(self.env.gpa, qualified_idx, .{ + .parent_qualified_idx = parent_name, + .item_name_idx = anno_ident, + }); + + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.idents.put(self.env.gpa, qualified_idx, placeholder_pattern_idx); + + // Register progressively qualified names at each scope level per the plan + try self.registerUserFacingName(qualified_idx, placeholder_pattern_idx); + } + }, + else => { + // Skip other statement types + }, + } + } + + // Phase 2b: Recursively create placeholders for nested associated blocks. + // This ensures all deeply nested items are registered in the module scope + // so qualified access like "ForwardDeep.M1.M2.deepVal" works from anywhere. + // Note: We only do the first pass here (placeholder creation). The actual + // body canonicalization for nested blocks happens in processAssociatedBlock + // AFTER the parent scope is entered, so nested scopes can access parent items. + for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { + const stmt = self.parse_ir.store.getStatement(stmt_idx); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + if (type_decl.associated) |assoc| { + const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; + const type_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; + const parent_text = self.env.getIdent(parent_name); + const type_text = self.env.getIdent(type_ident); + const qualified_idx = try self.env.insertQualifiedIdent(parent_text, type_text); + + // Build relative name for nested type (without module prefix) + const nested_relative_name = if (relative_parent_name) |rel_parent| blk: { + // Parent already has relative path, extend it + const rel_parent_text = self.env.getIdent(rel_parent); + break :blk try self.env.insertQualifiedIdent(rel_parent_text, type_text); + } else blk: { + // Parent is module root, so this type name IS the relative path + break :blk type_ident; + }; + + // Recursively create placeholders for this nested block's items + try self.processAssociatedItemsFirstPass(qualified_idx, nested_relative_name, assoc.statements); + } + } + } +} + +/// Canonicalizes a full Roc source file, transforming the Abstract Syntax Tree (AST) +/// into Canonical Intermediate Representation (CIR). +/// +/// This is the main entry point for file-level canonicalization, handling: +/// - Module headers and exposed items +/// - Type declarations (including nested types in associated blocks) +/// - Value definitions +/// - Import statements +/// - Module validation (type modules, default-app modules, etc.) +pub fn canonicalizeFile( + self: *Self, +) std.mem.Allocator.Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + // Assert that everything is in-sync + self.env.debugAssertArraysInSync(); + + const file = self.parse_ir.store.getFile(); + + // canonicalize_header_packages(); + + // First, process the header to populate exposed_idents/exposed_types and set module_kind + const header = self.parse_ir.store.getHeader(file.header); + switch (header) { + .module => |h| { + self.env.module_kind = .deprecated_module; + // Emit deprecation warning + const header_region = self.parse_ir.tokenizedRegionToRegion(h.region); + try self.env.pushDiagnostic(.{ + .module_header_deprecated = .{ + .region = header_region, + }, + }); + try self.createExposedScope(h.exposes); + }, + .package => |h| { + self.env.module_kind = .package; + try self.createExposedScope(h.exposes); + }, + .platform => |h| { + self.env.module_kind = .platform; + try self.createExposedScope(h.exposes); + // Also add the 'provides' items (what platform provides to the host, e.g., main_for_host!) + // These need to be in the exposed scope so they become exports + // Platform provides uses curly braces { main_for_host! } so it's parsed as record fields + try self.addPlatformProvidesItems(h.provides); + // Extract required type signatures for type checking using the new for-clause syntax + // This stores the types in env.requires_types without creating local definitions + // Also introduces type aliases (like Model) into the platform's top-level scope + try self.processRequiresEntries(h.requires_entries); + }, + .hosted => |h| { + self.env.module_kind = .hosted; + try self.createExposedScope(h.exposes); + }, + .app => |h| { + self.env.module_kind = .app; + // App headers have 'provides' instead of 'exposes' + // but we need to track the provided functions for export + try self.createExposedScope(h.provides); + }, + .type_module => { + // Set to undefined placeholder - will be properly set during validation + // when we find the matching type declaration + self.env.module_kind = .{ .type_module = undefined }; + // Type modules don't have an exposes list + // We'll validate the type name matches the module name after processing types + }, + .default_app => { + self.env.module_kind = .default_app; + // Default app modules don't have an exposes list + // They have a main! function that will be validated + }, + .malformed => { + self.env.module_kind = .malformed; + // Skip malformed headers + }, + } + + // Track the start of scratch defs and statements + const scratch_defs_start = self.env.store.scratchDefTop(); + const scratch_statements_start = self.env.store.scratch.?.statements.top(); + + // First pass (1a): Process type declarations WITH associated blocks to introduce them into scope + // Defer associated blocks themselves until after we've created placeholders for top-level items + for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + switch (stmt) { + .type_decl => |type_decl| { + if (type_decl.associated) |_| { + try self.processTypeDeclFirstPass(type_decl, null, null, true); // defer associated blocks + } + }, + .decl => |decl| { + // Introduce declarations for forawrd/recursive references + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const pattern_region = self.parse_ir.tokenizedRegionToRegion(pattern.to_tokenized_region()); + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + // Create placeholder pattern with qualified name + const placeholder_pattern = Pattern{ + .assign = .{ .ident = decl_ident }, + }; + const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, pattern_region); + + // Introduce the qualified name to scope + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, decl_ident, placeholder_pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = decl_ident, + .region = pattern_region, + .original_region = original_region, + } }); + }, + .top_level_var_error => { + // This shouldn't happen for declarations in associated blocks + }, + .var_across_function_boundary => { + // This shouldn't happen for declarations in associated blocks + }, + } + } + } + }, + else => { + // Skip non-type-declaration statements in first pass + }, + } + } + + // Phase 1.5.5: Process anno-only top-level type annotations EARLY + // For type-modules, anno-only top-level type annotations (like list_get_unsafe) need to be + // processed before associated blocks so they can be referenced inside those blocks + // IMPORTANT: Only process anno-only (no matching decl), and only for type-modules + switch (self.env.module_kind) { + .type_module => { + const top_level_stmts = self.parse_ir.store.statementSlice(file.statements); + var i: usize = 0; + while (i < top_level_stmts.len) : (i += 1) { + const stmt_id = top_level_stmts[i]; + const stmt = self.parse_ir.store.getStatement(stmt_id); + if (stmt == .type_anno) { + const ta = stmt.type_anno; + const name_ident = self.parse_ir.tokens.resolveIdentifier(ta.name) orelse continue; + + // Check if there's a matching decl (skipping malformed statements) + const has_matching_decl = blk: { + var next_i = i + 1; + while (next_i < top_level_stmts.len) : (next_i += 1) { + const next_stmt = self.parse_ir.store.getStatement(top_level_stmts[next_i]); + // Skip malformed statements + if (next_stmt == .malformed) continue; + // Check if this is a matching decl + if (next_stmt == .decl) { + const next_pattern = self.parse_ir.store.getPattern(next_stmt.decl.pattern); + if (next_pattern == .ident) { + if (self.parse_ir.tokens.resolveIdentifier(next_pattern.ident.ident_tok)) |decl_ident| { + break :blk name_ident.idx == decl_ident.idx; + } + } + } + // Found a non-malformed, non-matching statement + break :blk false; + } + // Reached end of statements + break :blk false; + }; + + // Skip if there's a matching decl - it will be processed normally + if (has_matching_decl) continue; + + const region = self.parse_ir.tokenizedRegionToRegion(ta.region); + + // Extract type variables and canonicalize the annotation + const type_vars_top: u32 = @intCast(self.scratch_idents.top()); + try self.extractTypeVarIdentsFromASTAnno(ta.anno, type_vars_top); + const type_var_scope = self.scopeEnterTypeVar(); + defer self.scopeExitTypeVar(type_var_scope); + const type_anno_idx = try self.canonicalizeTypeAnno(ta.anno, .inline_anno); + + // Canonicalize where clauses if present + const where_clauses = if (ta.where) |where_coll| blk: { + const where_slice = self.parse_ir.store.whereClauseSlice(.{ .span = self.parse_ir.store.getCollection(where_coll).span }); + const where_start = self.env.store.scratchWhereClauseTop(); + for (where_slice) |where_idx| { + const canonicalized_where = try self.canonicalizeWhereClause(where_idx, .inline_anno); + try self.env.store.addScratchWhereClause(canonicalized_where); + } + break :blk try self.env.store.whereClauseSpanFrom(where_start); + } else null; + + // Create the anno-only def immediately + const def_idx = try self.createAnnoOnlyDef(name_ident, type_anno_idx, where_clauses, region); + try self.env.store.addScratchDef(def_idx); + + // If exposed, register it + const ident_text = self.env.getIdent(name_ident); + if (self.exposed_ident_texts.contains(ident_text)) { + const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); + try self.env.setExposedNodeIndexById(name_ident, def_idx_u16); + } + } + } + }, + else => {}, + } + + // Phase 1.5.8: Introduce type names for NOMINAL types WITHOUT associated blocks + // This allows associated blocks (processed in Phase 1.6) to reference sibling types + // that are declared without associated blocks (e.g., Positive's negate -> Negative) + // We only introduce the name here; full processing happens in Phase 1.7 + // Note: We only do this for nominals, not aliases, because aliases may reference + // nested types that are only introduced in Phase 1.6 + for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + if (type_decl.associated == null and (type_decl.kind == .nominal or type_decl.kind == .@"opaque")) { + try self.introduceTypeNameOnly(type_decl); + } + } + } + + // Phase 1.6: Now process all deferred type declaration associated blocks + // processAssociatedBlock creates placeholders for associated items via processAssociatedItemsFirstPass + // This introduces nested types (like Foo.Bar) that other type declarations may reference + for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + if (type_decl.associated) |assoc| { + const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; + const type_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; + + // Build fully qualified name (e.g., "Builtin.Str") + // For type-modules where the main type name equals the module name, + // use just the module name to avoid "Builtin.Builtin" + const module_name_text = self.env.module_name; + const type_name_text = self.env.getIdent(type_ident); + const qualified_type_ident = if (std.mem.eql(u8, module_name_text, type_name_text)) + type_ident // Type-module: use unqualified name + else + try self.env.insertQualifiedIdent(module_name_text, type_name_text); + + // For module-level associated blocks, pass the type name as relative_parent + // so nested types get correct relative names like "TypeName.NestedType". + // Exception: The Builtin module's types (Str, Bool, etc.) should have simple names + // since they're implicitly imported into every module's scope. + const relative_parent = if (std.mem.eql(u8, module_name_text, "Builtin")) + null + else + type_ident; + try self.processAssociatedBlock(qualified_type_ident, relative_parent, type_ident, assoc, false); + } + } + } + + // Phase 1.7: Process type declarations WITHOUT associated blocks + // These can now reference nested types that were introduced in Phase 1.6 + for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + switch (stmt) { + .type_decl => |type_decl| { + if (type_decl.associated == null) { + try self.processTypeDeclFirstPass(type_decl, null, null, false); // no associated block to defer + } + }, + else => { + // Skip non-type-declaration statements + }, + } + } + + // Second pass: Process all other statements + const ast_stmt_idxs = self.parse_ir.store.statementSlice(file.statements); + var i: usize = 0; + while (i < ast_stmt_idxs.len) : (i += 1) { + const stmt_id = ast_stmt_idxs[i]; + const stmt = self.parse_ir.store.getStatement(stmt_id); + switch (stmt) { + .import => |import_stmt| { + _ = try self.canonicalizeImportStatement(import_stmt); + }, + .decl => |decl| { + _ = try self.canonicalizeStmtDecl(decl, null); + }, + .@"var" => |var_stmt| { + // Not valid at top-level + const string_idx = try self.env.insertString("var"); + const region = self.parse_ir.tokenizedRegionToRegion(var_stmt.region); + try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ + .stmt = string_idx, + .region = region, + } }); + }, + .expr => |expr_stmt| { + // Not valid at top-level + const string_idx = try self.env.insertString("expression"); + const region = self.parse_ir.tokenizedRegionToRegion(expr_stmt.region); + try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ + .stmt = string_idx, + .region = region, + } }); + }, + .crash => |crash_stmt| { + // Not valid at top-level + const string_idx = try self.env.insertString("crash"); + const region = self.parse_ir.tokenizedRegionToRegion(crash_stmt.region); + try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ + .stmt = string_idx, + .region = region, + } }); + }, + .dbg => |dbg_stmt| { + // Not valid at top-level + const string_idx = try self.env.insertString("dbg"); + const region = self.parse_ir.tokenizedRegionToRegion(dbg_stmt.region); + try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ + .stmt = string_idx, + .region = region, + } }); + }, + .expect => |e| { + // Top-level expect statement + const region = self.parse_ir.tokenizedRegionToRegion(e.region); + + // Canonicalize the expect expression + const can_expect = try self.canonicalizeExpr(e.body) orelse { + // If canonicalization fails, create a malformed expression + const malformed = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = region, + } }); + const expect_stmt = Statement{ .s_expect = .{ + .body = malformed, + } }; + const expect_stmt_idx = try self.env.addStatement(expect_stmt, region); + try self.env.store.addScratchStatement(expect_stmt_idx); + continue; + }; + + // Create expect statement + const expect_stmt = Statement{ .s_expect = .{ + .body = can_expect.idx, + } }; + const expect_stmt_idx = try self.env.addStatement(expect_stmt, region); + try self.env.store.addScratchStatement(expect_stmt_idx); + }, + .@"for" => |for_stmt| { + // Not valid at top-level + const string_idx = try self.env.insertString("for"); + const region = self.parse_ir.tokenizedRegionToRegion(for_stmt.region); + try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ + .stmt = string_idx, + .region = region, + } }); + }, + .@"while" => |while_stmt| { + // Not valid at top-level + const string_idx = try self.env.insertString("while"); + const region = self.parse_ir.tokenizedRegionToRegion(while_stmt.region); + try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ + .stmt = string_idx, + .region = region, + } }); + }, + .@"return" => |return_stmt| { + // Not valid at top-level + const string_idx = try self.env.insertString("return"); + const region = self.parse_ir.tokenizedRegionToRegion(return_stmt.region); + try self.env.pushDiagnostic(Diagnostic{ .invalid_top_level_statement = .{ + .stmt = string_idx, + .region = region, + } }); + }, + .type_decl => { + // Already processed in first pass, skip + }, + .type_anno => |ta| { + const region = self.parse_ir.tokenizedRegionToRegion(ta.region); + + // Top-level type annotation - store for connection to next declaration + const name_ident = self.parse_ir.tokens.resolveIdentifier(ta.name) orelse { + // Malformed identifier - skip this annotation + const feature = try self.env.insertString("handle malformed identifier for a type annotation"); + try self.env.pushDiagnostic(Diagnostic{ + .not_implemented = .{ + .feature = feature, + .region = region, + }, + }); + + continue; + }; + + // For type-modules, check if this is an anno-only annotation that was already processed in Phase 1.5.5 + // We need to check if there's a matching decl - if there isn't, this was processed early + switch (self.env.module_kind) { + .type_module => { + // Check if there's a matching decl (skipping malformed statements) + const has_matching_decl = blk: { + var check_i = i + 1; + while (check_i < ast_stmt_idxs.len) : (check_i += 1) { + const check_stmt = self.parse_ir.store.getStatement(ast_stmt_idxs[check_i]); + // Skip malformed statements + if (check_stmt == .malformed) continue; + // Check if this is a matching decl + if (check_stmt == .decl) { + const check_pattern = self.parse_ir.store.getPattern(check_stmt.decl.pattern); + if (check_pattern == .ident) { + if (self.parse_ir.tokens.resolveIdentifier(check_pattern.ident.ident_tok)) |decl_ident| { + break :blk name_ident.idx == decl_ident.idx; + } + } + } + // Found a non-malformed, non-matching statement + break :blk false; + } + // Reached end of statements + break :blk false; + }; + + // Skip if this is anno-only (no matching decl) - it was processed in Phase 1.5.5 + if (!has_matching_decl) { + continue; + } + }, + else => {}, } - // Store this annotation for the next declaration - last_type_anno = .{ - .name = name_ident, - .anno_idx = type_anno_idx, - .type_vars = DataSpan.empty(), - .where_clauses = where_clauses, - }; + // First, make the top of our scratch list + const type_vars_top: u32 = @intCast(self.scratch_idents.top()); + + // Extract type variables from the AST annotation + try self.extractTypeVarIdentsFromASTAnno(ta.anno, type_vars_top); + + // Enter a new type var scope + const type_var_scope = self.scopeEnterTypeVar(); + defer self.scopeExitTypeVar(type_var_scope); + std.debug.assert(type_var_scope.idx == 0); + + // Now canonicalize the annotation with type variables in scope + const type_anno_idx = try self.canonicalizeTypeAnno(ta.anno, .inline_anno); + + // Canonicalize where clauses if present + const where_clauses = if (ta.where) |where_coll| blk: { + const where_slice = self.parse_ir.store.whereClauseSlice(.{ .span = self.parse_ir.store.getCollection(where_coll).span }); + const where_start = self.env.store.scratchWhereClauseTop(); + + for (where_slice) |where_idx| { + const canonicalized_where = try self.canonicalizeWhereClause(where_idx, .inline_anno); + try self.env.store.addScratchWhereClause(canonicalized_where); + } + + break :blk try self.env.store.whereClauseSpanFrom(where_start); + } else null; + + // Now, check the next non-malformed stmt to see if it matches this anno + // We need to skip malformed statements that might appear between the annotation and declaration + // If we encounter malformed statements, it means the annotation itself had parse errors, + // so we should not attach it to the declaration to avoid confusing type errors + var next_i = i + 1; + var skipped_malformed = false; + while (next_i < ast_stmt_idxs.len) { + const next_stmt_id = ast_stmt_idxs[next_i]; + const next_stmt = self.parse_ir.store.getStatement(next_stmt_id); + + // Skip malformed statements + if (next_stmt == .malformed) { + skipped_malformed = true; + next_i += 1; + continue; + } + + // Found a non-malformed statement + switch (next_stmt) { + .decl => |decl| { + // Check if the declaration pattern matches the annotation name + const ast_pattern = self.parse_ir.store.getPattern(decl.pattern); + const names_match = if (ast_pattern == .ident) blk: { + const pattern_ident = ast_pattern.ident; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident.ident_tok)) |decl_ident| { + break :blk name_ident.idx == decl_ident.idx; + } + break :blk false; + } else false; + + if (names_match) { + i = next_i; + // If we skipped malformed statements, the annotation had parse errors; + // don't attach it (to avoid confusing type mismatch errors). + _ = try self.canonicalizeStmtDecl(decl, if (skipped_malformed) null else TypeAnnoIdent{ + .name = name_ident, + .anno_idx = type_anno_idx, + .where = where_clauses, + }); + } else { + // Names don't match - create an anno-only def for this annotation + // and let the next iteration handle the decl normally + const def_idx = try self.createAnnoOnlyDef(name_ident, type_anno_idx, where_clauses, region); + try self.env.store.addScratchDef(def_idx); + + // If this identifier should be exposed, register it + const ident_text = self.env.getIdent(name_ident); + if (self.exposed_ident_texts.contains(ident_text)) { + const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); + try self.env.setExposedNodeIndexById(name_ident, def_idx_u16); + } + } + }, + else => { + // If the next non-malformed stmt is not a decl, + // create a Def with an e_anno_only body + const def_idx = try self.createAnnoOnlyDef(name_ident, type_anno_idx, where_clauses, region); + try self.env.store.addScratchDef(def_idx); + + // If this identifier should be exposed, register it + const ident_text = self.env.getIdent(name_ident); + if (self.exposed_ident_texts.contains(ident_text)) { + const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); + try self.env.setExposedNodeIndexById(name_ident, def_idx_u16); + } + }, + } + break; + } + + // If we didn't find any next statement, create an anno-only def + // (This handles the case where the type annotation is the last statement in the file) + if (next_i >= ast_stmt_idxs.len) { + const def_idx = try self.createAnnoOnlyDef(name_ident, type_anno_idx, where_clauses, region); + try self.env.store.addScratchDef(def_idx); + + // If this identifier should be exposed, register it + const ident_text = self.env.getIdent(name_ident); + if (self.exposed_ident_texts.contains(ident_text)) { + const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); + try self.env.setExposedNodeIndexById(name_ident, def_idx_u16); + } + } }, - .malformed => |malformed| { + .malformed => { // We won't touch this since it's already a parse error. - _ = malformed; - last_type_anno = null; // Clear on non-annotation statement }, } } @@ -935,61 +2293,266 @@ pub fn canonicalizeFile( self.env.all_defs = try self.env.store.defSpanFrom(scratch_defs_start); self.env.all_statements = try self.env.store.statementSpanFrom(scratch_statements_start); + // Create the span of exported defs by finding definitions that correspond to exposed items + try self.populateExports(); + + // Compute dependency-based evaluation order using SCC analysis + const DependencyGraph = @import("DependencyGraph.zig"); + var graph = try DependencyGraph.buildDependencyGraph( + self.env, + self.env.all_defs, + self.env.gpa, + ); + defer graph.deinit(); + + const eval_order = try DependencyGraph.computeSCCs(&graph, self.env.gpa); + const eval_order_ptr = try self.env.gpa.create(DependencyGraph.EvaluationOrder); + eval_order_ptr.* = eval_order; + self.env.evaluation_order = eval_order_ptr; + // Assert that everything is in-sync self.env.debugAssertArraysInSync(); - - // Freeze the interners after canonicalization is complete - self.env.freezeInterners(); } -fn collectBoundVars(self: *Self, pattern_idx: Pattern.Idx, bound_vars: *std.AutoHashMapUnmanaged(Pattern.Idx, void)) !void { +/// Validate a type module for use in checking mode (roc check). +/// This accepts both type modules and default-app modules, providing helpful +/// error messages when neither is valid. +pub fn validateForChecking(self: *Self) std.mem.Allocator.Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + switch (self.env.module_kind) { + .type_module => |*main_type_ident| { + const main_status = try self.checkMainFunction(); + const matching_type_ident = self.findMatchingTypeIdent(); + + // Store the matching type ident in module_kind if found + if (matching_type_ident) |type_ident| { + main_type_ident.* = type_ident; + // The main type and associated items are already exposed in canonicalize() + } + + // Valid if either we have a valid main! or a matching type declaration + const is_valid = (main_status == .valid) or (matching_type_ident != null); + + if (!is_valid and main_status == .not_found) { + // Neither valid main! nor matching type - report helpful error + try self.reportTypeModuleOrDefaultAppError(); + } + }, + .default_app, .app, .package, .platform, .hosted, .deprecated_module, .malformed => { + // No validation needed for these module kinds in checking mode + }, + } +} + +/// Validate a module for use in execution mode (e.g. `roc main.roc` or `roc build`). +/// Requires a valid main! function for type_module headers. +pub fn validateForExecution(self: *Self) std.mem.Allocator.Error!void { + switch (self.env.module_kind) { + .type_module => { + const main_status = try self.checkMainFunction(); + if (main_status == .not_found) { + try self.reportExecutionRequiresAppOrDefaultApp(); + } + }, + .default_app, .app, .package, .platform, .hosted, .deprecated_module, .malformed => { + // No validation needed for these module kinds in execution mode + }, + } +} + +/// Creates an annotation-only def for a standalone type annotation with no implementation +fn createAnnoOnlyDef( + self: *Self, + ident: base.Ident.Idx, + type_anno_idx: TypeAnno.Idx, + where_clauses: ?WhereClause.Span, + region: Region, +) std.mem.Allocator.Error!CIR.Def.Idx { + // Check if a placeholder exists for this identifier (from multi-phase canonicalization) + const pattern_idx = if (self.isPlaceholder(ident)) placeholder_check: { + // Use scopeLookup to search up the scope chain for the placeholder + switch (self.scopeLookup(.ident, ident)) { + .found => |existing_pattern| { + // Note: We don't remove from placeholder_idents here. The calling code + // (processAssociatedItemsSecondPass) will call updatePlaceholder to do that. + break :placeholder_check existing_pattern; + }, + .not_found => { + // Placeholder is tracked but not found in current scope chain. + // This can happen if the placeholder was created in a scope that's + // not an ancestor of the current scope. Create a new pattern as fallback; + // any actual errors will be caught later during definition checking. + const pattern = Pattern{ + .assign = .{ + .ident = ident, + }, + }; + break :placeholder_check try self.env.addPattern(pattern, region); + }, + } + } else create_new: { + // No placeholder - create new pattern and introduce to scope + const pattern = Pattern{ + .assign = .{ + .ident = ident, + }, + }; + const new_pattern_idx = try self.env.addPattern(pattern, region); + + // Introduce the identifier to scope so it can be referenced + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident, new_pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = ident, + .region = region, + .original_region = original_region, + } }); + }, + else => {}, + } + break :create_new new_pattern_idx; + }; + + // Note: We don't update placeholders here. For associated items, the calling code + // (processAssociatedItemsSecondPass) will update all three identifiers (qualified, + // type-qualified, unqualified). For top-level items, there are no placeholders to update. + + // Create the e_anno_only expression + const anno_only_expr = try self.env.addExpr(Expr{ .e_anno_only = .{} }, region); + + // Create the annotation structure + const annotation = CIR.Annotation{ + .anno = type_anno_idx, + .where = where_clauses, + }; + const annotation_idx = try self.env.addAnnotation(annotation, region); + + // Create and return the def + return try self.env.addDef(.{ + .pattern = pattern_idx, + .expr = anno_only_expr, + .annotation = annotation_idx, + .kind = .let, + }, region); +} + +fn canonicalizeStmtDecl(self: *Self, decl: AST.Statement.Decl, mb_last_anno: ?TypeAnnoIdent) std.mem.Allocator.Error!void { + // Check if this declaration matches the last type annotation + var mb_validated_anno: ?Annotation.Idx = null; + if (mb_last_anno) |anno_info| { + const ast_pattern = self.parse_ir.store.getPattern(decl.pattern); + if (ast_pattern == .ident) { + const pattern_ident = ast_pattern.ident; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident.ident_tok)) |decl_ident| { + if (anno_info.name.idx == decl_ident.idx) { + // This declaration matches the type annotation + const pattern_region = self.parse_ir.tokenizedRegionToRegion(ast_pattern.to_tokenized_region()); + mb_validated_anno = try self.createAnnotationFromTypeAnno(anno_info.anno_idx, anno_info.where, pattern_region); + } + } + // Note: If resolveIdentifier returns null, the identifier token is malformed. + // The parser already handles this; we just don't match it with the annotation. + } + } + + // Canonicalize the decl (with the validated anno) + const def_idx = try self.canonicalizeDeclWithAnnotation(decl, mb_validated_anno); + try self.env.store.addScratchDef(def_idx); + + // If this declaration successfully defined an exposed value, remove it from exposed_ident_texts + // and add the node index to exposed_items + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const token_region = self.parse_ir.tokens.resolve(@intCast(pattern.ident.ident_tok)); + const ident_text = self.parse_ir.env.source[token_region.start.offset..token_region.end.offset]; + + // Top-level associated items (identifiers ending with '!') are automatically exposed + const is_associated_item = ident_text.len > 0 and ident_text[ident_text.len - 1] == '!'; + + // If this identifier is exposed (or is an associated item), add it to exposed_items + if (self.exposed_ident_texts.contains(ident_text) or is_associated_item) { + // Get the interned identifier - it should already exist from parsing + const ident = base.Ident.for_text(ident_text); + const idx = try self.env.insertIdent(ident); + // Store the def index as u16 in exposed_items + const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); + try self.env.setExposedNodeIndexById(idx, def_idx_u16); + } + + _ = self.exposed_ident_texts.remove(ident_text); + } +} + +/// An annotation and it's scope. This struct owns the Scope +const AnnotationAndScope = struct { + anno_idx: Annotation.Idx, + scope: *Scope, +}; + +const TypeAnnoIdent = struct { + name: base.Ident.Idx, + anno_idx: TypeAnno.Idx, + where: ?WhereClause.Span, +}; + +fn collectBoundVarsToScratch(self: *Self, pattern_idx: Pattern.Idx) !void { const pattern = self.env.store.getPattern(pattern_idx); switch (pattern) { .assign => { - try bound_vars.put(self.env.gpa, pattern_idx, {}); + try self.scratch_bound_vars.append(pattern_idx); }, .record_destructure => |destructure| { for (self.env.store.sliceRecordDestructs(destructure.destructs)) |destruct_idx| { const destruct = self.env.store.getRecordDestruct(destruct_idx); switch (destruct.kind) { - .Required => |sub_pattern_idx| try self.collectBoundVars(sub_pattern_idx, bound_vars), - .SubPattern => |sub_pattern_idx| try self.collectBoundVars(sub_pattern_idx, bound_vars), + .Required => |sub_pattern_idx| try self.collectBoundVarsToScratch(sub_pattern_idx), + .SubPattern => |sub_pattern_idx| try self.collectBoundVarsToScratch(sub_pattern_idx), } } }, .tuple => |tuple| { for (self.env.store.slicePatterns(tuple.patterns)) |elem_pattern_idx| { - try self.collectBoundVars(elem_pattern_idx, bound_vars); + try self.collectBoundVarsToScratch(elem_pattern_idx); } }, .applied_tag => |tag| { for (self.env.store.slicePatterns(tag.args)) |arg_pattern_idx| { - try self.collectBoundVars(arg_pattern_idx, bound_vars); + try self.collectBoundVarsToScratch(arg_pattern_idx); } }, .as => |as_pat| { - try bound_vars.put(self.env.gpa, pattern_idx, {}); - try self.collectBoundVars(as_pat.pattern, bound_vars); + try self.scratch_bound_vars.append(pattern_idx); + try self.collectBoundVarsToScratch(as_pat.pattern); }, .list => |list| { for (self.env.store.slicePatterns(list.patterns)) |elem_idx| { - try self.collectBoundVars(elem_idx, bound_vars); + try self.collectBoundVarsToScratch(elem_idx); } if (list.rest_info) |rest| { if (rest.pattern) |rest_pat_idx| { - try self.collectBoundVars(rest_pat_idx, bound_vars); + try self.collectBoundVarsToScratch(rest_pat_idx); } } }, - .int_literal, + .nominal => |nom| { + // Recurse into the backing pattern to collect bound variables + try self.collectBoundVarsToScratch(nom.backing_pattern); + }, + .nominal_external => |nom| { + // Recurse into the backing pattern to collect bound variables + try self.collectBoundVarsToScratch(nom.backing_pattern); + }, + .num_literal, .small_dec_literal, .dec_literal, .frac_f32_literal, .frac_f64_literal, .str_literal, .underscore, - .nominal, - .nominal_external, .runtime_error, => {}, } @@ -999,11 +2562,20 @@ fn createExposedScope( self: *Self, exposes: AST.Collection.Idx, ) std.mem.Allocator.Error!void { - const gpa = self.env.gpa; + // Clear exposed sets (they're already initialized with default values) + self.exposed_idents.clearRetainingCapacity(); + self.exposed_types.clearRetainingCapacity(); - // Reset exposed_scope (already initialized in init) - self.exposed_scope.deinit(gpa); - self.exposed_scope = Scope.init(false); + try self.addToExposedScope(exposes); +} + +/// Add items to the exposed scope without resetting it. +/// Used for platforms which have both 'exposes' (for apps) and 'provides' (for the host). +fn addToExposedScope( + self: *Self, + exposes: AST.Collection.Idx, +) std.mem.Allocator.Error!void { + const gpa = self.env.gpa; const collection = self.parse_ir.store.getCollection(exposes); const exposed_items = self.parse_ir.store.exposedItemSlice(.{ .span = collection.span }); @@ -1031,9 +2603,8 @@ fn createExposedScope( // Add to exposed_items for permanent storage (unconditionally) try self.env.addExposedById(ident_idx); - // Use a dummy pattern index - we just need to track that it's exposed - const dummy_idx = @as(Pattern.Idx, @enumFromInt(0)); - try self.exposed_scope.put(gpa, .ident, ident_idx, dummy_idx); + // Just track that this identifier is exposed + try self.exposed_idents.put(gpa, ident_idx, {}); } // Store by text in a temporary hash map, since indices may change @@ -1061,12 +2632,11 @@ fn createExposedScope( // Get the interned identifier if (self.parse_ir.tokens.resolveIdentifier(type_name.ident)) |ident_idx| { - // Add to exposed_items for permanent storage (unconditionally) - try self.env.addExposedById(ident_idx); + // Don't add types to exposed_items - types are not values + // Only add to type_bindings for type resolution - // Use a dummy statement index - we just need to track that it's exposed - const dummy_idx = @as(Statement.Idx, @enumFromInt(0)); - try self.exposed_scope.put(gpa, .type_decl, ident_idx, dummy_idx); + // Just track that this type is exposed + try self.exposed_types.put(gpa, ident_idx, {}); } // Store by text in a temporary hash map, since indices may change @@ -1094,12 +2664,11 @@ fn createExposedScope( // Get the interned identifier if (self.parse_ir.tokens.resolveIdentifier(type_with_constructors.ident)) |ident_idx| { - // Add to exposed_items for permanent storage (unconditionally) - try self.env.addExposedById(ident_idx); + // Don't add types to exposed_items - types are not values + // Only add to type_bindings for type resolution - // Use a dummy statement index - we just need to track that it's exposed - const dummy_idx = @as(Statement.Idx, @enumFromInt(0)); - try self.exposed_scope.put(gpa, .type_decl, ident_idx, dummy_idx); + // Just track that this type is exposed + try self.exposed_types.put(gpa, ident_idx, {}); } // Store by text in a temporary hash map, since indices may change @@ -1120,14 +2689,194 @@ fn createExposedScope( try self.exposed_type_texts.put(gpa, type_text, region); } }, - .malformed => |malformed| { + .malformed => { // Malformed exposed items are already captured as diagnostics during parsing - _ = malformed; }, } } } +/// Add platform provides items to the exposed scope. +/// Platform provides uses curly braces { main_for_host!: "main" } so it's parsed as record fields. +/// The string value is the FFI symbol name exported to the host (becomes roc__). +fn addPlatformProvidesItems( + self: *Self, + provides: AST.Collection.Idx, +) std.mem.Allocator.Error!void { + const gpa = self.env.gpa; + + const collection = self.parse_ir.store.getCollection(provides); + const record_fields = self.parse_ir.store.recordFieldSlice(.{ .span = collection.span }); + + for (record_fields) |field_idx| { + const field = self.parse_ir.store.getRecordField(field_idx); + + // Get the identifier text from the field name token + if (self.parse_ir.tokens.resolveIdentifier(field.name)) |ident_idx| { + // Add to exposed_items for permanent storage + try self.env.addExposedById(ident_idx); + + // Track that this identifier is exposed (for exports) + try self.exposed_idents.put(gpa, ident_idx, {}); + + // Also track in exposed_ident_texts + const token_region = self.parse_ir.tokens.resolve(@intCast(field.name)); + const ident_text = self.parse_ir.env.source[token_region.start.offset..token_region.end.offset]; + const region = self.parse_ir.tokenizedRegionToRegion(field.region); + _ = try self.exposed_ident_texts.getOrPut(gpa, ident_text); + if (self.exposed_ident_texts.getPtr(ident_text)) |ptr| { + ptr.* = region; + } + } + } +} + +/// Process the requires entries from a platform header using the new for-clause syntax. +/// +/// This extracts the required type signatures from the platform header and stores them +/// in `env.requires_types`. These are used during app type checking to ensure the app's +/// provided values match the platform's expected types. +/// +/// The new syntax is: requires { [Model : model] for main : () -> { init : ... } } +/// +/// For each requires entry, this function: +/// 1. Introduces the rigid type variables (e.g., `model`) into a temporary scope +/// 2. Creates aliases (e.g., `Model`) that refer to the SAME type as the rigid +/// 3. Canonicalizes the entrypoint type annotation with rigids in scope +/// 4. Stores the required type for type checking +/// +/// IMPORTANT: Both the rigid (`model`) and the alias (`Model`) reference the +/// same rigid variable +fn processRequiresEntries(self: *Self, requires_entries: AST.RequiresEntry.Span) std.mem.Allocator.Error!void { + for (self.parse_ir.store.requiresEntrySlice(requires_entries)) |entry_idx| { + const entry = self.parse_ir.store.getRequiresEntry(entry_idx); + const entry_region = self.parse_ir.tokenizedRegionToRegion(entry.region); + + // Enter a type var scope for the rigids in this entry + const type_var_scope = self.scopeEnterTypeVar(); + defer self.scopeExitTypeVar(type_var_scope); + + // Record start of type aliases for this entry + const type_aliases_start = self.env.for_clause_aliases.len(); + + // Process type aliases: [Model : model, Foo : foo] + // For each alias: + // 1. Create a type annotation for the rigid and introduce it to the + // type var scope + // 2. Create a type alias, pointing to the rigid and introduce at the + // root scope + // 3. Store the alias for later use during type checking + for (self.parse_ir.store.forClauseTypeAliasSlice(entry.type_aliases)) |alias_idx| { + const alias = self.parse_ir.store.getForClauseTypeAlias(alias_idx); + const alias_region = self.parse_ir.tokenizedRegionToRegion(alias.region); + + // Get the rigid name (lowercase, e.g., "model") + const rigid_name = self.parse_ir.tokens.resolveIdentifier(alias.rigid_name) orelse continue; + + // Get the alias name (uppercase, e.g., "Model") + const alias_name = self.parse_ir.tokens.resolveIdentifier(alias.alias_name) orelse continue; + + // Create a SINGLE type annotation for this rigid variable + // IMPORTANT: We use the rigid_name in the annotation, but introduce it + // under BOTH names in the scope + const rigid_anno_idx = try self.env.addTypeAnno(.{ .rigid_var = .{ + .name = rigid_name, + } }, alias_region); + + // Introduce the rigid (model) into the type variable scope + _ = try self.scopeIntroduceTypeVar(rigid_name, rigid_anno_idx); + + // IMPORTANT: Also introduce Model as a type alias in the module-level scope. + // This allows platform functions to use `Box(Model)` in their type signatures. + // The alias points to the same rigid annotation, which will be unified with + // the app's concrete type during type checking. + // + // Create a type header for the alias (no type parameters) + const alias_header = CIR.TypeHeader{ + .name = alias_name, + .relative_name = alias_name, + .args = .{ .span = .{ .start = 0, .len = 0 } }, + }; + const alias_header_idx = try self.env.addTypeHeader(alias_header, alias_region); + + // Create an s_alias_decl statement: Model : model + const alias_stmt = Statement{ + .s_alias_decl = .{ + .header = alias_header_idx, + .anno = rigid_anno_idx, + }, + }; + const alias_stmt_idx = try self.env.addStatement(alias_stmt, alias_region); + + // Add to the module-level scope (index 0) as a local_alias binding + // This makes Model available for use in type annotations throughout the platform module + const module_scope = &self.scopes.items[0]; + try module_scope.type_bindings.put(self.env.gpa, alias_name, Scope.TypeBinding{ + .local_alias = alias_stmt_idx, + }); + + // Store the alias mapping for use during type checking + _ = try self.env.for_clause_aliases.append(self.env.gpa, .{ + .alias_name = alias_name, + .rigid_name = rigid_name, + .alias_stmt_idx = alias_stmt_idx, + }); + } + + // Calculate type aliases range for this entry + const type_aliases_end = self.env.for_clause_aliases.len(); + const type_aliases_range = ModuleEnv.ForClauseAlias.SafeList.Range{ + .start = @enumFromInt(type_aliases_start), + .count = @intCast(type_aliases_end - type_aliases_start), + }; + + // Get the entrypoint name (e.g., "main") + const entrypoint_name = self.parse_ir.tokens.resolveIdentifier(entry.entrypoint_name) orelse continue; + + // Canonicalize the type annotation for this entrypoint + // + // IMPORTANT: We set the context here to be type_decl_anno so we + // correctly get errors if the annotation tries to introduce a rigid var + // that's not in scope + var type_anno_ctx = TypeAnnoCtx.init(.type_decl_anno); + const type_anno_idx = try self.canonicalizeTypeAnnoHelp(entry.type_anno, &type_anno_ctx); + + // Store the required type in the module env + _ = try self.env.requires_types.append(self.env.gpa, .{ + .ident = entrypoint_name, + .type_anno = type_anno_idx, + .region = entry_region, + .type_aliases = type_aliases_range, + }); + } +} + +fn populateExports(self: *Self) std.mem.Allocator.Error!void { + // Start a new scratch space for exports + const scratch_exports_start = self.env.store.scratchDefTop(); + + // Use the already-created all_defs span + const defs_slice = self.env.store.sliceDefs(self.env.all_defs); + + // Check each definition to see if it corresponds to an exposed item. + // We check exposed_idents which only contains items from the exposing clause, + // not associated items like "Color.as_str" which are registered separately. + for (defs_slice) |def_idx| { + const def = self.env.store.getDef(def_idx); + const pattern = self.env.store.getPattern(def.pattern); + + if (pattern == .assign) { + // Check if this identifier was explicitly exposed in the module header + if (self.exposed_idents.contains(pattern.assign.ident)) { + try self.env.store.addScratchDef(def_idx); + } + } + } + + // Create the exports span from the scratch space + self.env.exports = try self.env.store.defSpanFrom(scratch_exports_start); +} + fn checkExposedButNotImplemented(self: *Self) std.mem.Allocator.Error!void { // Check for remaining exposed identifiers var ident_iter = self.exposed_ident_texts.iterator(); @@ -1161,90 +2910,213 @@ fn checkExposedButNotImplemented(self: *Self) std.mem.Allocator.Error!void { } } -fn bringImportIntoScope( +/// Process a module import with common logic shared by explicit imports and auto-imports. +/// This handles everything after module name and alias resolution. +/// Process import with an alias (normal import like `import json.Json` or `import json.Json as J`) +fn importAliased( self: *Self, - import: *const AST.Statement, -) void { - // const gpa = self.env.gpa; - // const import_name: []u8 = &.{}; // import.module_name_tok; - // const shorthand: []u8 = &.{}; // import.qualifier_tok; - // const region = Region{ - // .start = Region.Position.zero(), - // .end = Region.Position.zero(), - // }; + module_name: Ident.Idx, + alias_tok: ?Token.Idx, + exposed_items_span: CIR.ExposedItem.Span, + import_region: Region, + is_package_qualified: bool, +) std.mem.Allocator.Error!?Statement.Idx { + const module_name_text = self.env.getIdent(module_name); - // const res = self.env.imports.getOrInsert(gpa, import_name, shorthand); - - // if (res.was_present) { - // _ = self.env.problems.append(gpa, Problem.Canonicalize.make(.{ .DuplicateImport = .{ - // .duplicate_import_region = region, - // } })); - // } - - const exposesSlice = self.parse_ir.store.exposedItemSlice(import.exposes); - for (exposesSlice) |exposed_idx| { - const exposed = self.parse_ir.store.getExposedItem(exposed_idx); - switch (exposed) { - .lower_ident => |ident| { - - // TODO handle `as` here using an Alias - - if (self.parse_ir.tokens.resolveIdentifier(ident.ident)) |ident_idx| { - _ = ident_idx; - - // TODO Introduce our import - - // _ = self.scope.levels.introduce(gpa, &self.env.idents, .ident, .{ .scope_name = ident_idx, .ident = ident_idx }); - } - }, - .upper_ident => |imported_type| { - _ = imported_type; - // const alias = Alias{ - // .name = imported_type.name, - // .region = ir.env.tag_names.getRegion(imported_type.name), - // .is_builtin = false, - // .kind = .ImportedUnknown, - // }; - // const alias_idx = ir.aliases.append(alias); - // - // _ = scope.levels.introduce(.alias, .{ - // .scope_name = imported_type.name, - // .alias = alias_idx, - // }); - }, - .upper_ident_star => |ident| { - _ = ident; - }, - } - } -} - -fn bringIngestedFileIntoScope( - self: *Self, - import: *const parse.AST.Stmt.Import, -) void { - const res = self.env.modules.getOrInsert( - import.name, - import.package_shorthand, + // 1. Get or create Import.Idx for this module (with ident for index-based lookups) + const module_import_idx = try self.env.imports.getOrPutWithIdent( + self.env.gpa, + self.env.common.getStringStore(), + module_name_text, + module_name, ); - if (res.was_present) { - // _ = self.env.problems.append(Problem.Canonicalize.make(.DuplicateImport{ - // .duplicate_import_region = import.name_region, - // })); + // 2. Resolve the alias + const alias = try self.resolveModuleAlias(alias_tok, module_name) orelse return null; + + // 3. Add to scope: alias -> module_name mapping (includes is_package_qualified flag) + try self.scopeIntroduceModuleAlias(alias, module_name, import_region, exposed_items_span, is_package_qualified); + + // 4. Process type imports from this module + try self.processTypeImports(module_name, alias); + + // 5. Introduce exposed items into scope (includes auto-expose for type modules) + try self.introduceItemsAliased(exposed_items_span, module_name, alias, import_region, module_import_idx); + + // 6. Store the mapping from module name to Import.Idx + try self.import_indices.put(self.env.gpa, module_name_text, module_import_idx); + + // 7. Create CIR import statement + const cir_import = Statement{ + .s_import = .{ + .module_name_tok = module_name, + .qualifier_tok = null, + .alias_tok = null, + .exposes = exposed_items_span, + }, + }; + + const import_idx = try self.env.addStatement(cir_import, import_region); + try self.env.store.addScratchStatement(import_idx); + + // 8. Add the module to the current scope so it can be used in qualified lookups + const current_scope = self.currentScope(); + _ = try current_scope.introduceImportedModule(self.env.gpa, module_name_text, module_import_idx); + + // 9. Check that this module actually exists, and if not report an error + // Only check if module_envs is provided - when it's null, we don't know what modules + // exist yet (e.g., during standalone module canonicalization without full project context) + // Skip for package-qualified imports (e.g., "pf.Stdout") - those are cross-package + // imports that are resolved by the workspace resolver + if (self.module_envs) |envs_map| { + if (!envs_map.contains(module_name)) { + if (!is_package_qualified) { + try self.env.pushDiagnostic(Diagnostic{ .module_not_found = .{ + .module_name = module_name, + .region = import_region, + } }); + } + } } - // scope.introduce(self: *Scope, comptime item_kind: Level.ItemKind, ident: Ident.Idx) + // If this import satisfies an exposed type requirement (e.g., platform re-exporting + // an imported module), remove it from exposed_type_texts so we don't report + // "EXPOSED BUT NOT DEFINED" for re-exported imports. + _ = self.exposed_type_texts.remove(module_name_text); - for (import.exposing.items.items) |exposed| { - const exposed_ident = switch (exposed) { - .Value => |ident| ident, - .Type => |ident| ident, - .CustomTagUnion => |custom| custom.name, - }; - self.env.addExposedIdentForModule(exposed_ident, res.module_idx); - // TODO: Implement scope introduction for exposed identifiers + return import_idx; +} + +/// Process import with an alias provided directly as an Ident.Idx (used for auto-imports) +fn importWithAlias( + self: *Self, + module_name: Ident.Idx, + alias: Ident.Idx, + exposed_items_span: CIR.ExposedItem.Span, + import_region: Region, +) std.mem.Allocator.Error!Statement.Idx { + const module_name_text = self.env.getIdent(module_name); + + // 1. Get or create Import.Idx for this module (with ident for index-based lookups) + const module_import_idx = try self.env.imports.getOrPutWithIdent( + self.env.gpa, + self.env.common.getStringStore(), + module_name_text, + module_name, + ); + + // 2. Add to scope: alias -> module_name mapping + try self.scopeIntroduceModuleAlias(alias, module_name, import_region, exposed_items_span); + + // 3. Process type imports from this module + try self.processTypeImports(module_name, alias); + + // 4. Introduce exposed items into scope (includes auto-expose for type modules) + try self.introduceItemsAliased(exposed_items_span, module_name, alias, import_region, module_import_idx); + + // 5. Store the mapping from module name to Import.Idx + try self.import_indices.put(self.env.gpa, module_name_text, module_import_idx); + + // 6. Create CIR import statement + const cir_import = Statement{ + .s_import = .{ + .module_name_tok = module_name, + .qualifier_tok = null, + .alias_tok = null, + .exposes = exposed_items_span, + }, + }; + + const import_idx = try self.env.addStatement(cir_import, import_region); + try self.env.store.addScratchStatement(import_idx); + + // 7. Add the module to the current scope so it can be used in qualified lookups + const current_scope = self.currentScope(); + _ = try current_scope.introduceImportedModule(self.env.gpa, module_name_text, module_import_idx); + + // 8. Check that this module actually exists, and if not report an error + // Only check if module_envs is provided - when it's null, we don't know what modules + // exist yet (e.g., during standalone module canonicalization without full project context) + if (self.module_envs) |envs_map| { + if (!envs_map.contains(module_name)) { + try self.env.pushDiagnostic(Diagnostic{ .module_not_found = .{ + .module_name = module_name, + .region = import_region, + } }); + } } + + // If this import satisfies an exposed type requirement (e.g., platform re-exporting + // an imported module), remove it from exposed_type_texts so we don't report + // "EXPOSED BUT NOT DEFINED" for re-exported imports. + _ = self.exposed_type_texts.remove(module_name_text); + + return import_idx; +} + +/// Process auto-expose import without alias (like `import json.Parser.Config`) +fn importUnaliased( + self: *Self, + module_name: Ident.Idx, + exposed_items_span: CIR.ExposedItem.Span, + import_region: Region, + is_package_qualified: bool, +) std.mem.Allocator.Error!Statement.Idx { + const module_name_text = self.env.getIdent(module_name); + + // 1. Get or create Import.Idx for this module (with ident for index-based lookups) + const module_import_idx = try self.env.imports.getOrPutWithIdent( + self.env.gpa, + self.env.common.getStringStore(), + module_name_text, + module_name, + ); + + // 2. Introduce exposed items into scope (no alias, no auto-expose of main type) + try self.introduceItemsUnaliased(exposed_items_span, module_name, import_region, module_import_idx); + + // 3. Store the mapping from module name to Import.Idx + try self.import_indices.put(self.env.gpa, module_name_text, module_import_idx); + + // 4. Create CIR import statement + const cir_import = Statement{ + .s_import = .{ + .module_name_tok = module_name, + .qualifier_tok = null, + .alias_tok = null, + .exposes = exposed_items_span, + }, + }; + + const import_idx = try self.env.addStatement(cir_import, import_region); + try self.env.store.addScratchStatement(import_idx); + + // 5. Add the module to the current scope so it can be used in qualified lookups + const current_scope = self.currentScope(); + _ = try current_scope.introduceImportedModule(self.env.gpa, module_name_text, module_import_idx); + + // 6. Check that this module actually exists, and if not report an error + // Only check if module_envs is provided - when it's null, we don't know what modules + // exist yet (e.g., during standalone module canonicalization without full project context) + // Skip for package-qualified imports (e.g., "pf.Stdout") - those are cross-package + // imports that are resolved by the workspace resolver + if (self.module_envs) |envs_map| { + if (!envs_map.contains(module_name)) { + if (!is_package_qualified) { + try self.env.pushDiagnostic(Diagnostic{ .module_not_found = .{ + .module_name = module_name, + .region = import_region, + } }); + } + } + } + + // If this import satisfies an exposed type requirement (e.g., platform re-exporting + // an imported module), remove it from exposed_type_texts so we don't report + // "EXPOSED BUT NOT DEFINED" for re-exported imports. + _ = self.exposed_type_texts.remove(module_name_text); + + return import_idx; } /// Canonicalize an import statement, handling both top-level file imports and statement imports @@ -1300,8 +3172,11 @@ fn canonicalizeImportStatement( .region = region, } }); - // Use a placeholder identifier instead - const placeholder_text = "MALFORMED_IMPORT"; + // Use a unique placeholder identifier that starts with '#' to ensure it can't + // collide with user-defined identifiers (# starts a comment in Roc) + var buf: [32]u8 = undefined; + const placeholder_text = std.fmt.bufPrint(&buf, "#malformed_import_{d}", .{self.malformed_import_count}) catch unreachable; + self.malformed_import_count += 1; break :blk try self.env.insertIdent(base.Ident.for_text(placeholder_text)); } } else { @@ -1310,66 +3185,20 @@ fn canonicalizeImportStatement( } }; - // 2. Determine the alias (either explicit or default to last part) - const alias = try self.resolveModuleAlias(import_stmt.alias_tok, module_name) orelse return null; - - // 3. Get or create Import.Idx for this module - const module_name_text = self.env.getIdent(module_name); - const module_import_idx = try self.env.imports.getOrPut( - self.env.gpa, - self.env.common.getStringStore(), - module_name_text, - ); - - // 4. Add to scope: alias -> module_name mapping - try self.scopeIntroduceModuleAlias(alias, module_name); - - // Process type imports from this module - try self.processTypeImports(module_name, alias); - - // 5. Convert exposed items and introduce them into scope - const cir_exposes = try self.convertASTExposesToCIR(import_stmt.exposes); + // 2. Convert exposed items to CIR + const scratch_start = self.env.store.scratchExposedItemTop(); + try self.convertASTExposesToCIR(import_stmt.exposes); + const cir_exposes = try self.env.store.exposedItemSpanFrom(scratch_start); const import_region = self.parse_ir.tokenizedRegionToRegion(import_stmt.region); - try self.introduceExposedItemsIntoScope(cir_exposes, module_name, import_region); - // 6. Store the mapping from module name to Import.Idx - try self.import_indices.put(self.env.gpa, module_name_text, module_import_idx); + // 3. Check if this is a package-qualified import (has a qualifier like "pf" in "pf.Stdout") + const is_package_qualified = import_stmt.qualifier_tok != null; - // 7. Create CIR import statement - const cir_import = Statement{ - .s_import = .{ - .module_name_tok = module_name, - .qualifier_tok = if (import_stmt.qualifier_tok) |q_tok| self.parse_ir.tokens.resolveIdentifier(q_tok) else null, - .alias_tok = if (import_stmt.alias_tok) |a_tok| self.parse_ir.tokens.resolveIdentifier(a_tok) else null, - .exposes = cir_exposes, - }, - }; - - const import_idx = try self.env.addStatementAndTypeVar(cir_import, Content{ .flex_var = null }, self.parse_ir.tokenizedRegionToRegion(import_stmt.region)); - try self.env.store.addScratchStatement(import_idx); - - // 8. Add the module to the current scope so it can be used in qualified lookups - const current_scope = self.currentScope(); - _ = try current_scope.introduceImportedModule(self.env.gpa, module_name_text, module_import_idx); - - // 9. Check that this module actually exists, and if not report an error - if (self.module_envs) |envs_map| { - // Check if the module exists - if (!envs_map.contains(module_name_text)) { - // Module not found - create diagnostic - try self.env.pushDiagnostic(Diagnostic{ .module_not_found = .{ - .module_name = module_name, - .region = import_region, - } }); - } - } else { - try self.env.pushDiagnostic(Diagnostic{ .module_not_found = .{ - .module_name = module_name, - .region = import_region, - } }); - } - - return import_idx; + // 4. Dispatch to the appropriate handler based on whether this is a nested import + return if (import_stmt.nested_import) + try self.importUnaliased(module_name, cir_exposes, import_region, is_package_qualified) + else + try self.importAliased(module_name, import_stmt.alias_tok, cir_exposes, import_region, is_package_qualified); } /// Resolve the module alias name from either explicit alias or module name @@ -1395,12 +3224,7 @@ fn createQualifiedName( const module_text = self.env.getIdent(module_name); const field_text = self.env.getIdent(field_name); - // Allocate space for "module.field" - this case still needs allocation since we're combining - // module name from import with field name from usage site - const qualified_text = try std.fmt.allocPrint(self.env.gpa, "{s}.{s}", .{ module_text, field_text }); - defer self.env.gpa.free(qualified_text); - - return try self.env.insertIdent(base.Ident.for_text(qualified_text), Region.zero()); + return try self.env.insertQualifiedIdent(module_text, field_text); } /// Create an external declaration for a qualified name @@ -1426,12 +3250,11 @@ fn createExternalDeclaration( } /// Convert AST exposed items to CIR exposed items +/// If main_type_name is provided, auto-inject it as an exposed item fn convertASTExposesToCIR( self: *Self, ast_exposes: AST.ExposedItem.Span, -) std.mem.Allocator.Error!CIR.ExposedItem.Span { - const scratch_start = self.env.store.scratchExposedItemTop(); - +) std.mem.Allocator.Error!void { const ast_exposed_slice = self.parse_ir.store.exposedItemSlice(ast_exposes); for (ast_exposed_slice) |ast_exposed_idx| { const ast_exposed = self.parse_ir.store.getExposedItem(ast_exposed_idx); @@ -1479,35 +3302,96 @@ fn convertASTExposesToCIR( inline else => |payload| payload.region, }; const region = self.parse_ir.tokenizedRegionToRegion(tokenized_region); - const cir_exposed_idx = try self.env.addExposedItemAndTypeVar(cir_exposed, .{ .flex_var = null }, region); + const cir_exposed_idx = try self.env.addExposedItem(cir_exposed, region); try self.env.store.addScratchExposedItem(cir_exposed_idx); } - - return try self.env.store.exposedItemSpanFrom(scratch_start); } -/// Introduce converted exposed items into scope for identifier resolution -fn introduceExposedItemsIntoScope( +/// Introduce converted exposed items into scope for aliased imports +/// For imports like `import json.Parser exposing [Config]`, this will: +/// 1. Auto-expose the module's main type if it's a type module +/// 2. Process explicitly exposed items +fn introduceItemsAliased( self: *Self, exposed_items_span: CIR.ExposedItem.Span, module_name: Ident.Idx, + module_alias: Ident.Idx, import_region: Region, + module_import_idx: CIR.Import.Idx, ) std.mem.Allocator.Error!void { const exposed_items_slice = self.env.store.sliceExposedItems(exposed_items_span); + const current_scope = self.currentScope(); - // If we have module_envs, validate the imports if (self.module_envs) |envs_map| { - const module_name_text = self.env.getIdent(module_name); + const module_entry = envs_map.get(module_name) orelse { + // Module not found, but still check for duplicate type names with auto-imports + // This ensures we report DUPLICATE DEFINITION even for non-existent modules + for (exposed_items_slice) |exposed_item_idx| { + const exposed_item = self.env.store.getExposedItem(exposed_item_idx); + const local_ident = exposed_item.alias orelse exposed_item.name; - // Check if the module exists - if (!envs_map.contains(module_name_text)) { - // Module not found - Module existence check is already done in canonicalizeImportStatement, - // so there is no need to create another diagnostic here for module_not_found + // Check if this conflicts with an existing type binding (e.g., auto-imported type) + if (current_scope.type_bindings.get(local_ident)) |existing_binding| { + const original_region = switch (existing_binding) { + .external_nominal => |ext| ext.origin_region, + else => Region.zero(), + }; + + try self.env.pushDiagnostic(Diagnostic{ + .shadowing_warning = .{ + .ident = local_ident, + .region = import_region, + .original_region = original_region, + }, + }); + } + } return; - } + }; + const module_env = module_entry.env; - // Get the module's exposed_items - const module_env = envs_map.get(module_name_text).?; + // Auto-expose the module's main type for type modules + switch (module_env.module_kind) { + .type_module => |main_type_ident| { + if (module_env.containsExposedById(main_type_ident)) { + const item_info = Scope.ExposedItemInfo{ + .module_name = module_name, + .original_name = main_type_ident, + }; + try self.scopeIntroduceExposedItem(module_alias, item_info, import_region); + + // Get the correct target_node_idx using statement_idx from module_envs + const target_node_idx = blk: { + // Use the already-captured envs_map from the outer scope + if (envs_map.get(module_name)) |auto_imported| { + if (auto_imported.statement_idx) |stmt_idx| { + if (module_env.getExposedNodeIndexByStatementIdx(stmt_idx)) |node_idx| { + break :blk node_idx; + } + } + } + // Fallback to the old method if we can't find it via statement_idx + break :blk module_env.getExposedNodeIndexById(main_type_ident); + }; + + // Get the type name text from the target module's ident store + const original_type_name = module_env.getIdent(main_type_ident); + + try self.setExternalTypeBinding( + current_scope, + module_alias, + module_name, + main_type_ident, + original_type_name, + target_node_idx, + module_import_idx, + import_region, + .module_was_found, + ); + } + }, + else => {}, + } // Validate each exposed item for (exposed_items_slice) |exposed_item_idx| { @@ -1550,7 +3434,7 @@ fn introduceExposedItemsIntoScope( .module_name = module_name, .original_name = exposed_item.name, }; - try self.scopeIntroduceExposedItem(item_name, item_info); + try self.scopeIntroduceExposedItem(item_name, item_info, import_region); } } else { // No module_envs provided, introduce all items without validation @@ -1561,73 +3445,213 @@ fn introduceExposedItemsIntoScope( .module_name = module_name, .original_name = exposed_item.name, }; - try self.scopeIntroduceExposedItem(item_name, item_info); + try self.scopeIntroduceExposedItem(item_name, item_info, import_region); } } } +/// Introduce converted exposed items into scope for auto-expose imports +/// For imports like `import json.Parser.Config`, this will: +/// 1. Skip auto-exposing the module's main type (no alias exists) +/// 2. Process only explicitly exposed items +fn introduceItemsUnaliased( + self: *Self, + exposed_items_span: CIR.ExposedItem.Span, + module_name: Ident.Idx, + import_region: Region, + module_import_idx: CIR.Import.Idx, +) std.mem.Allocator.Error!void { + const exposed_items_slice = self.env.store.sliceExposedItems(exposed_items_span); + const current_scope = self.currentScope(); + + if (self.module_envs) |envs_map| { + const module_entry = envs_map.get(module_name) orelse { + // Module not found, but still check for duplicate type names with auto-imports + // This ensures we report DUPLICATE DEFINITION even for non-existent modules + for (exposed_items_slice) |exposed_item_idx| { + const exposed_item = self.env.store.getExposedItem(exposed_item_idx); + const local_ident = exposed_item.alias orelse exposed_item.name; + + // Check if this conflicts with an existing type binding (e.g., auto-imported type) + if (current_scope.type_bindings.get(local_ident)) |existing_binding| { + const original_region = switch (existing_binding) { + .external_nominal => |ext| ext.origin_region, + else => Region.zero(), + }; + + try self.env.pushDiagnostic(Diagnostic{ + .shadowing_warning = .{ + .ident = local_ident, + .region = import_region, + .original_region = original_region, + }, + }); + } + } + return; + }; + const module_env = module_entry.env; + + // No auto-expose of main type - only process explicitly exposed items + for (exposed_items_slice) |exposed_item_idx| { + const exposed_item = self.env.store.getExposedItem(exposed_item_idx); + const local_ident = exposed_item.alias orelse exposed_item.name; + const local_name_text = self.env.getIdent(local_ident); + + const target_ident = module_env.common.findIdent(self.env.getIdent(exposed_item.name)); + const is_type_name = local_name_text.len > 0 and local_name_text[0] >= 'A' and local_name_text[0] <= 'Z'; + + if (target_ident) |ident_in_module| { + if (!module_env.containsExposedById(ident_in_module)) { + if (is_type_name) { + try self.env.pushDiagnostic(Diagnostic{ .type_not_exposed = .{ + .module_name = module_name, + .type_name = exposed_item.name, + .region = import_region, + } }); + } else { + try self.env.pushDiagnostic(Diagnostic{ .value_not_exposed = .{ + .module_name = module_name, + .value_name = exposed_item.name, + .region = import_region, + } }); + } + continue; + } + + const target_node_idx = module_env.getExposedNodeIndexById(ident_in_module) orelse { + if (is_type_name) { + try self.env.pushDiagnostic(Diagnostic{ .type_not_exposed = .{ + .module_name = module_name, + .type_name = exposed_item.name, + .region = import_region, + } }); + } else { + try self.env.pushDiagnostic(Diagnostic{ .value_not_exposed = .{ + .module_name = module_name, + .value_name = exposed_item.name, + .region = import_region, + } }); + } + continue; + }; + + const item_info = Scope.ExposedItemInfo{ + .module_name = module_name, + .original_name = exposed_item.name, + }; + try self.scopeIntroduceExposedItem(local_ident, item_info, import_region); + + if (is_type_name) { + // Get the original type name text from current module's ident store + const original_type_name = self.env.getIdent(exposed_item.name); + + try self.setExternalTypeBinding( + current_scope, + local_ident, + module_name, + exposed_item.name, + original_type_name, + target_node_idx, + module_import_idx, + import_region, + .module_was_found, + ); + } + } else { + if (local_name_text.len > 0 and local_name_text[0] >= 'A' and local_name_text[0] <= 'Z') { + try self.env.pushDiagnostic(Diagnostic{ .type_not_exposed = .{ + .module_name = module_name, + .type_name = exposed_item.name, + .region = import_region, + } }); + } else { + try self.env.pushDiagnostic(Diagnostic{ .value_not_exposed = .{ + .module_name = module_name, + .value_name = exposed_item.name, + .region = import_region, + } }); + } + } + } + } else { + for (exposed_items_slice) |exposed_item_idx| { + const exposed_item = self.env.store.getExposedItem(exposed_item_idx); + const local_ident = exposed_item.alias orelse exposed_item.name; + const local_name_text = self.env.getIdent(local_ident); + const item_info = Scope.ExposedItemInfo{ + .module_name = module_name, + .original_name = exposed_item.name, + }; + try self.scopeIntroduceExposedItem(local_ident, item_info, import_region); + + if (local_name_text.len > 0 and local_name_text[0] >= 'A' and local_name_text[0] <= 'Z') { + // Get the original type name text from current module's ident store + const original_type_name = self.env.getIdent(exposed_item.name); + + try self.setExternalTypeBinding( + current_scope, + local_ident, + module_name, + exposed_item.name, + original_type_name, + null, + module_import_idx, + import_region, + .module_not_found, + ); + } + } + } +} + +/// Canonicalize a decl with an annotation fn canonicalizeDeclWithAnnotation( self: *Self, decl: AST.Statement.Decl, - annotation: ?Annotation.Idx, + mb_anno_idx: ?Annotation.Idx, ) std.mem.Allocator.Error!CIR.Def.Idx { const trace = tracy.trace(@src()); defer trace.end(); - const pattern_region = self.parse_ir.tokenizedRegionToRegion(self.parse_ir.store.getPattern(decl.pattern).to_tokenized_region()); - const expr_region = self.parse_ir.tokenizedRegionToRegion(self.parse_ir.store.getExpr(decl.body).to_tokenized_region()); - + // Either find the placeholder pattern insert in the first past if ident, + // otherwise canonicalize the pattern + const pattern = self.parse_ir.store.getPattern(decl.pattern); const pattern_idx = blk: { - if (try self.canonicalizePattern(decl.pattern)) |idx| { - break :blk idx; + if (pattern == .ident) { + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + // Look up the placeholder pattern that was created in the first pass + const lookup_result = self.scopeLookup(.ident, decl_ident); + switch (lookup_result) { + .found => |pattern_idx| break :blk pattern_idx, + .not_found => unreachable, // Pattern should have been created in first pass + } + } else { + break :blk try self.canonicalizePatternOrMalformed(decl.pattern); + } } else { - const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .pattern_not_canonicalized = .{ - .region = pattern_region, - } }); - break :blk malformed_idx; + break :blk try self.canonicalizePatternOrMalformed(decl.pattern); } }; - const can_expr = blk: { - if (try self.canonicalizeExpr(decl.body)) |ce| { - break :blk ce; - } else { - const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ - .region = expr_region, - } }); - break :blk CanonicalizedExpr{ .idx = malformed_idx, .free_vars = null }; - } - }; - const expr_idx = can_expr.idx; + const can_expr = try self.canonicalizeExprOrMalformed(decl.body); - // Create the def entry and set def type variable to a flex var - // - // We always use a flex variable for the definition, regardless of whether there's - // an annotation. This is because: - // 1. If there's no annotation, we need a flex var for normal type inference - // 2. If there IS an annotation, we still use a flex var to avoid copying the - // annotation's type content. This is necessary because if the annotation contains - // an alias (e.g., `empty : ConsList(a)`), that alias expects its type arguments - // to live at specific memory offsets relative to the alias's own type variable. - // Copying the alias content to a different type variable would break this assumption. - // 3. During type checking, the definition's flex var will be unified with the - // annotation's type (if present) or with the inferred type from the expression - // 4. Type errors will be caught during unification if the implementation doesn't - // match the annotation const region = self.parse_ir.tokenizedRegionToRegion(decl.region); - const def_idx = self.env.addDefAndTypeVar(.{ + const def_idx = self.env.addDef(.{ .pattern = pattern_idx, - .expr = expr_idx, - .annotation = annotation, + .expr = can_expr.idx, + .annotation = mb_anno_idx, .kind = .let, - }, Content{ .flex_var = null }, region); + }, region); return def_idx; } fn parseSingleQuoteCodepoint( inner_text: []const u8, -) ?u21 { +) u21 { + // tokenizer checks for valid single quote codepoints, so every error case is unreachable here const escaped = inner_text[0] == '\\'; if (escaped) { @@ -1635,13 +3659,9 @@ fn parseSingleQuoteCodepoint( switch (c) { 'u' => { const hex_code = inner_text[3 .. inner_text.len - 1]; - const codepoint = std.fmt.parseInt(u21, hex_code, 16) catch { - return null; - }; + const codepoint = std.fmt.parseInt(u21, hex_code, 16) catch unreachable; - if (!std.unicode.utf8ValidCodepoint(codepoint)) { - return null; - } + std.debug.assert(std.unicode.utf8ValidCodepoint(codepoint)); return codepoint; }, @@ -1657,32 +3677,22 @@ fn parseSingleQuoteCodepoint( 't' => { return '\t'; }, - else => { - return null; - }, + else => unreachable, } } else { - const view = std.unicode.Utf8View.init(inner_text) catch |err| switch (err) { - error.InvalidUtf8 => { - return null; - }, - }; + const view = std.unicode.Utf8View.init(inner_text) catch unreachable; var iterator = view.iterator(); - if (iterator.nextCodepoint()) |codepoint| { - std.debug.assert(iterator.nextCodepoint() == null); - return codepoint; - } else { - // only single valid utf8 codepoint can be here after tokenization - unreachable; - } + const codepoint = iterator.nextCodepoint().?; + std.debug.assert(iterator.nextCodepoint() == null); + return codepoint; } } fn canonicalizeStringLike( self: *Self, - e: anytype, + e: AST.Expr.StringLike, is_multiline: bool, ) std.mem.Allocator.Error!CanonicalizedExpr { // Get all the string parts @@ -1700,12 +3710,12 @@ fn canonicalizeStringLike( try self.extractStringSegments(parts); const region = self.parse_ir.tokenizedRegionToRegion(e.region); - const expr_idx = try self.env.addExprAndTypeVar(Expr{ .e_str = .{ + const expr_idx = try self.env.addExpr(Expr{ .e_str = .{ .span = can_str_span, - } }, Content{ .structure = .str }, region); + } }, region); - const free_vars_slice = self.scratch_free_vars.slice(free_vars_start, self.scratch_free_vars.top()); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_slice.len > 0) free_vars_slice else null }; + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; } fn canonicalizeSingleQuote( @@ -1718,38 +3728,30 @@ fn canonicalizeSingleQuote( // Resolve to a string slice from the source const token_text = self.parse_ir.resolve(token); + std.debug.assert(token_text[0] == '\'' and token_text[token_text.len - 1] == '\''); - if (parseSingleQuoteCodepoint(token_text[1 .. token_text.len - 1])) |codepoint| { - const type_content = Content{ .structure = .{ .num = .{ .num_unbound = types.Num.IntRequirements{ - .sign_needed = false, - .bits_needed = @intCast(@sizeOf(u21)), - } } } }; - const value_content = CIR.IntValue{ - .bytes = @bitCast(@as(u128, @intCast(codepoint))), - .kind = .u128, - }; - if (Idx == Expr.Idx) { - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ - .e_int = .{ - .value = value_content, - }, - }, type_content, region); - return expr_idx; - } else if (Idx == Pattern.Idx) { - const pat_idx = try self.env.addPatternAndTypeVar(Pattern{ - .int_literal = .{ - .value = value_content, - }, - }, type_content, region); - return pat_idx; - } else { - @compileError("Unsupported Idx type"); - } + const codepoint = parseSingleQuoteCodepoint(token_text[1 .. token_text.len - 1]); + const value_content = CIR.IntValue{ + .bytes = @bitCast(@as(u128, @intCast(codepoint))), + .kind = .u128, + }; + if (comptime Idx == Expr.Idx) { + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_num = .{ + .value = value_content, + .kind = .int_unbound, + }, + }, region); + return expr_idx; + } else if (comptime Idx == Pattern.Idx) { + const pat_idx = try self.env.addPattern(Pattern{ .num_literal = .{ + .value = value_content, + .kind = .int_unbound, + } }, region); + return pat_idx; + } else { + @compileError("Unsupported Idx type"); } - - return try self.env.pushMalformed(Idx, Diagnostic{ .invalid_single_quote = .{ - .region = region, - } }); } fn canonicalizeRecordField( @@ -1789,7 +3791,7 @@ fn canonicalizeRecordField( .value = can_value.idx, }; - return try self.env.addRecordFieldAndTypeVar(cir_field, Content{ .flex_var = null }, self.parse_ir.tokenizedRegionToRegion(field.region)); + return try self.env.addRecordField(cir_field, self.parse_ir.tokenizedRegionToRegion(field.region)); } /// Parse an integer with underscores. @@ -1831,19 +3833,59 @@ pub fn canonicalizeExpr( return can_expr; } + // Check if this is a type var alias dispatch (e.g., Thing.default({})) + if (ast_fn == .ident) { + const ident_expr = ast_fn.ident; + const qualifier_tokens = self.parse_ir.store.tokenSlice(ident_expr.qualifiers); + if (qualifier_tokens.len == 1) { + const qualifier_tok = @as(Token.Idx, @intCast(qualifier_tokens[0])); + if (self.parse_ir.tokens.resolveIdentifier(qualifier_tok)) |alias_name| { + // Look up in all scopes + for (self.scopes.items) |*scope| { + const lookup_result = scope.lookupTypeVarAlias(alias_name); + switch (lookup_result) { + .found => |binding| { + // This is a type var alias dispatch with args! + // Get the method name from the ident + if (self.parse_ir.tokens.resolveIdentifier(ident_expr.token)) |method_name| { + // Canonicalize the arguments + const scratch_top = self.env.store.scratchExprTop(); + const args_slice = self.parse_ir.store.exprSlice(e.args); + for (args_slice) |arg| { + if (try self.canonicalizeExpr(arg)) |can_arg| { + try self.env.store.addScratchExpr(can_arg.idx); + } + } + const args_span = try self.env.store.exprSpanFrom(scratch_top); + + // Create e_type_var_dispatch expression with args + const dispatch_expr_idx = try self.env.addExpr(CIR.Expr{ .e_type_var_dispatch = .{ + .type_var_alias_stmt = binding.statement_idx, + .method_name = method_name, + .args = args_span, + } }, region); + + return CanonicalizedExpr{ .idx = dispatch_expr_idx, .free_vars = DataSpan.empty() }; + } + }, + .not_found => {}, // Continue checking other scopes + } + } + } + } + } + // Not a tag application, proceed with normal function call // Mark the start of scratch expressions const free_vars_start = self.scratch_free_vars.top(); - const scratch_top = self.env.store.scratchExprTop(); // Canonicalize the function being called and add as first element const can_fn_expr = try self.canonicalizeExpr(e.@"fn") orelse { - self.env.store.clearScratchExprsFrom(scratch_top); return null; }; - try self.env.store.addScratchExpr(can_fn_expr.idx); // Canonicalize and add all arguments + const scratch_top = self.env.store.scratchExprTop(); const args_slice = self.parse_ir.store.exprSlice(e.args); for (args_slice) |arg| { if (try self.canonicalizeExpr(arg)) |can_arg| { @@ -1854,92 +3896,337 @@ pub fn canonicalizeExpr( // Create span from scratch expressions const args_span = try self.env.store.exprSpanFrom(scratch_top); - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_call = .{ + .func = can_fn_expr.idx, .args = args_span, .called_via = CalledVia.apply, }, - }, Content{ .flex_var = null }, region); + }, region); - const free_vars_slice = self.scratch_free_vars.slice(free_vars_start, self.scratch_free_vars.top()); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_slice.len > 0) free_vars_slice else null }; + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; }, .ident => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); if (self.parse_ir.tokens.resolveIdentifier(e.token)) |ident| { // Check if this is a module-qualified identifier const qualifier_tokens = self.parse_ir.store.tokenSlice(e.qualifiers); - if (qualifier_tokens.len > 0) { + blk_qualified: { + if (qualifier_tokens.len == 0) break :blk_qualified; + // First, try looking up the full qualified name as a local identifier (for associated items) + const strip_tokens = [_]tokenize.Token.Tag{.NoSpaceDotLowerIdent}; + const qualified_name_text = self.parse_ir.resolveQualifiedName( + e.qualifiers, + e.token, + &strip_tokens, + ); + const qualified_ident = try self.env.insertIdent(base.Ident.for_text(qualified_name_text)); + + // Single-lookup approach: look up the qualified name exactly as written. + // Registration puts progressively qualified names in each scope, so this should find it. + switch (self.scopeLookup(.ident, qualified_ident)) { + .found => |found_pattern_idx| { + // Mark this pattern as used for unused variable checking + try self.used_patterns.put(self.env.gpa, found_pattern_idx, {}); + + // We found the qualified ident in local scope + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_local = .{ + .pattern_idx = found_pattern_idx, + } }, region); + + const free_vars_start = self.scratch_free_vars.top(); + try self.scratch_free_vars.append(found_pattern_idx); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.init(free_vars_start, 1) }; + }, + .not_found => { + // Not found locally - check if first qualifier is a module alias for external lookup + }, + } + const qualifier_tok = @as(Token.Idx, @intCast(qualifier_tokens[0])); if (self.parse_ir.tokens.resolveIdentifier(qualifier_tok)) |module_alias| { - // Check if this is a module alias - if (self.scopeLookupModule(module_alias)) |module_name| { + // Check if this is a type variable alias first (e.g., Thing.default where Thing : thing) + if (qualifier_tokens.len == 1) { + // Look up in all scopes, not just current scope + for (self.scopes.items) |*scope| { + const lookup_result = scope.lookupTypeVarAlias(module_alias); + switch (lookup_result) { + .found => |binding| { + // This is a type var alias dispatch! + // Get the method name from the ident (e.g., "default") + const method_name = ident; + + // Create e_type_var_dispatch expression + const dispatch_expr_idx = try self.env.addExpr(CIR.Expr{ + .e_type_var_dispatch = .{ + .type_var_alias_stmt = binding.statement_idx, + .method_name = method_name, + .args = .{ .span = .{ .start = 0, .len = 0 } }, // No args for now; filled in by apply + }, + }, region); + + return CanonicalizedExpr{ .idx = dispatch_expr_idx, .free_vars = DataSpan.empty() }; + }, + .not_found => {}, // Continue checking other scopes + } + } + } + + // Check if this is a module alias, or an auto-imported module + const module_info: ?Scope.ModuleAliasInfo = self.scopeLookupModule(module_alias) orelse blk: { + // Not in scope, check if it's an auto-imported module + if (self.module_envs) |envs_map| { + if (envs_map.contains(module_alias)) { + // This is an auto-imported module like Bool or Try + // Use the module_alias directly as the module_name (not package-qualified) + break :blk Scope.ModuleAliasInfo{ + .module_name = module_alias, + .is_package_qualified = false, + }; + } + } + break :blk null; + }; + const module_name = if (module_info) |info| info.module_name else { + // Not a module alias and not an auto-imported module + // Check if the qualifier is a type - if so, try to lookup associated items + const is_type_in_scope = self.scopeLookupTypeBinding(module_alias) != null; + const is_auto_imported_type = if (self.module_envs) |envs_map| + envs_map.contains(module_alias) + else + false; + + if (is_type_in_scope or is_auto_imported_type) { + // This is a type with a potential associated item + // Build the fully qualified name and try to look it up + const type_text = self.env.getIdent(module_alias); + const field_text = self.env.getIdent(ident); + const type_qualified_idx = try self.env.insertQualifiedIdent(type_text, field_text); + + // For auto-imported types (like Str, Bool from Builtin module), + // we need to look up the method in the Builtin module, not current scope + if (is_auto_imported_type and self.module_envs != null) { + if (self.module_envs.?.get(module_alias)) |auto_imported_type_env| { + const module_env = auto_imported_type_env.env; + + // Get the qualified name of the method (e.g., "Str.is_empty") + const qualified_text = self.env.getIdent(type_qualified_idx); + + // Try to find the method in the Builtin module's exposed items + if (module_env.common.findIdent(qualified_text)) |qname_ident| { + if (module_env.getExposedNodeIndexById(qname_ident)) |target_node_idx| { + // Found it! This is a module-qualified lookup + // Need to get or create the auto-import for the Builtin module + const actual_module_name = module_env.module_name; + const import_idx = try self.getOrCreateAutoImport(actual_module_name); + + // Create e_lookup_external expression + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_external = .{ + .module_idx = import_idx, + .target_node_idx = target_node_idx, + .region = region, + } }, region); + + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; + } + } + } + } + + // For types in current scope, try current scope lookup + switch (self.scopeLookup(.ident, type_qualified_idx)) { + .found => |found_pattern_idx| { + // Found the associated item! Mark it as used. + try self.used_patterns.put(self.env.gpa, found_pattern_idx, {}); + + // Return a local lookup expression + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_local = .{ + .pattern_idx = found_pattern_idx, + } }, region); + + const free_vars_start = self.scratch_free_vars.top(); + try self.scratch_free_vars.append(found_pattern_idx); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.init(free_vars_start, 1) }; + }, + .not_found => { + // Associated item not found - generate error + if (trace_modules) { + const parent_text = self.env.getIdent(module_alias); + const nested_text = self.env.getIdent(ident); + std.debug.print("[TRACE-MODULES] nested_value_not_found: {s}.{s} (scope lookup failed)\n", .{ parent_text, nested_text }); + } + const diagnostic = Diagnostic{ .nested_value_not_found = .{ + .parent_name = module_alias, + .nested_name = ident, + .region = region, + } }; + return CanonicalizedExpr{ + .idx = try self.env.pushMalformed(Expr.Idx, diagnostic), + .free_vars = DataSpan.empty(), + }; + }, + } + } + + // Not a type either - generate appropriate error + const diagnostic = Diagnostic{ .qualified_ident_does_not_exist = .{ + .ident = qualified_ident, + .region = region, + } }; + + return CanonicalizedExpr{ + .idx = try self.env.pushMalformed(Expr.Idx, diagnostic), + .free_vars = DataSpan.empty(), + }; + }; + + { // This is a module-qualified lookup const module_text = self.env.getIdent(module_name); // Check if this module is imported in the current scope - const import_idx = self.scopeLookupImportedModule(module_text) orelse { + // For auto-imported nested types (Bool, Str), use the parent module name (Builtin) + const lookup_module_name = if (self.module_envs) |envs_map| blk_lookup: { + if (envs_map.get(module_name)) |auto_imported_type| { + break :blk_lookup auto_imported_type.env.module_name; + } else { + break :blk_lookup module_text; + } + } else module_text; + + // If not, create an auto-import + const import_idx = self.scopeLookupImportedModule(lookup_module_name) orelse blk: { + // Check if this is an auto-imported module + if (self.module_envs) |envs_map| { + if (envs_map.get(module_name)) |auto_imported_type| { + // For auto-imported nested types (like Bool, Str), import the parent module (Builtin) + const actual_module_name = auto_imported_type.env.module_name; + break :blk try self.getOrCreateAutoImport(actual_module_name); + } + } + // Module not imported in current scope return CanonicalizedExpr{ .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .module_not_imported = .{ .module_name = module_name, .region = region, } }), - .free_vars = null, + .free_vars = DataSpan.empty(), }; }; // Look up the target node index in the module's exposed_items // Need to convert identifier from current module to target module const field_text = self.env.getIdent(ident); - const target_node_idx = if (self.module_envs) |envs_map| blk: { - if (envs_map.get(module_text)) |module_env| { - if (module_env.common.findIdent(field_text)) |target_ident| { - break :blk module_env.getExposedNodeIndexById(target_ident) orelse 0; - } else { - break :blk 0; - } + + const target_node_idx_opt: ?u16 = if (self.module_envs) |envs_map| blk: { + if (envs_map.get(module_name)) |auto_imported_type| { + const module_env = auto_imported_type.env; + + // For auto-imported types with statement_idx (builtin types and platform modules), + // build the full qualified name using qualified_type_ident. + // For regular user module imports (statement_idx is null), use field_text directly. + const lookup_name: []const u8 = if (auto_imported_type.statement_idx) |_| name_blk: { + // Build the fully qualified member name using the type's qualified ident + // e.g., for U8.to_i16: "Builtin.Num.U8" + "to_i16" -> "Builtin.Num.U8.to_i16" + // e.g., for Str.concat: "Builtin.Str" + "concat" -> "Builtin.Str.concat" + // Note: qualified_type_ident is always stored in the calling module's ident store + // (self.env), since Ident.Idx values are not transferable between stores. + const qualified_text = self.env.getIdent(auto_imported_type.qualified_type_ident); + const fully_qualified_idx = try self.env.insertQualifiedIdent(qualified_text, field_text); + break :name_blk self.env.getIdent(fully_qualified_idx); + } else field_text; + + // Look up the associated item by its name + const qname_ident = module_env.common.findIdent(lookup_name) orelse { + // Identifier not found - just return null + // The error will be handled by the code below that checks target_node_idx_opt + break :blk null; + }; + break :blk module_env.getExposedNodeIndexById(qname_ident); } else { - break :blk 0; + break :blk null; } - } else 0; + } else null; + + const target_node_idx = target_node_idx_opt orelse { + // The identifier doesn't exist in the module or isn't exposed + // Check if the module is in module_envs - if not, the import failed (MODULE NOT FOUND) + // and we shouldn't report a redundant error here + const module_exists = if (self.module_envs) |envs_map| + envs_map.contains(module_name) + else + false; + + if (!module_exists) { + // Module import failed, don't generate redundant error + // Fall through to normal identifier lookup + break :blk_qualified; + } + + // Generate a more helpful error for auto-imported types (List, Bool, Try, etc.) + const is_auto_imported_type = if (self.module_envs) |envs_map| + envs_map.contains(module_name) + else + false; + + const diagnostic = if (is_auto_imported_type) + Diagnostic{ .nested_value_not_found = .{ + .parent_name = module_name, + .nested_name = ident, + .region = region, + } } + else + Diagnostic{ .qualified_ident_does_not_exist = .{ + .ident = qualified_ident, + .region = region, + } }; + + return CanonicalizedExpr{ + .idx = try self.env.pushMalformed(Expr.Idx, diagnostic), + .free_vars = DataSpan.empty(), + }; + }; // Create the e_lookup_external expression with Import.Idx - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ .e_lookup_external = .{ + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_external = .{ .module_idx = import_idx, .target_node_idx = target_node_idx, .region = region, - } }, Content{ .flex_var = null }, region); + } }, region); return CanonicalizedExpr{ .idx = expr_idx, - .free_vars = null, + .free_vars = DataSpan.empty(), }; } } - } + } // end blk_qualified // Not a module-qualified lookup, or qualifier not found, proceed with normal lookup switch (self.scopeLookup(.ident, ident)) { - .found => |pattern_idx| { + .found => |found_pattern_idx| { // Mark this pattern as used for unused variable checking - try self.used_patterns.put(self.env.gpa, pattern_idx, {}); + try self.used_patterns.put(self.env.gpa, found_pattern_idx, {}); // Check if this is a used underscore variable try self.checkUsedUnderscoreVariable(ident, region); - // We found the ident in scope, lookup to reference the pattern - const expr_idx = try self.env.addExprAndTypeVarRedirect(CIR.Expr{ .e_lookup_local = .{ - .pattern_idx = pattern_idx, - } }, ModuleEnv.varFrom(pattern_idx), region); + // We found the ident in scope, create a lookup to reference the pattern + // Note: Rank tracking for let-polymorphism is handled by the type checker (Check.zig) + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_local = .{ + .pattern_idx = found_pattern_idx, + } }, region); const free_vars_start = self.scratch_free_vars.top(); - try self.scratch_free_vars.append(self.env.gpa, pattern_idx); - const free_vars_slice = self.scratch_free_vars.slice(free_vars_start, self.scratch_free_vars.top()); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_slice.len > 0) free_vars_slice else null }; + try self.scratch_free_vars.append(found_pattern_idx); + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; }, .not_found => { // Check if this identifier is an exposed item from an import if (self.scopeLookupExposedItem(ident)) |exposed_info| { + // Get the Import.Idx for the module this item comes from const module_text = self.env.getIdent(exposed_info.module_name); const import_idx = self.scopeLookupImportedModule(module_text) orelse { @@ -1949,41 +4236,124 @@ pub fn canonicalizeExpr( .module_name = exposed_info.module_name, .region = region, } }), - .free_vars = null, + .free_vars = DataSpan.empty(), }; }; // Look up the target node index in the module's exposed_items // Need to convert identifier from current module to target module const field_text = self.env.getIdent(exposed_info.original_name); - const target_node_idx = if (self.module_envs) |envs_map| blk: { - if (envs_map.get(module_text)) |module_env| { + const target_node_idx_opt: ?u16 = if (self.module_envs) |envs_map| blk: { + if (envs_map.get(exposed_info.module_name)) |auto_imported_type| { + const module_env = auto_imported_type.env; if (module_env.common.findIdent(field_text)) |target_ident| { - break :blk module_env.getExposedNodeIndexById(target_ident) orelse 0; + break :blk module_env.getExposedNodeIndexById(target_ident); } else { - break :blk 0; + break :blk null; } } else { - break :blk 0; + break :blk null; } - } else 0; + } else null; - // Create the e_lookup_external expression with Import.Idx - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ .e_lookup_external = .{ - .module_idx = import_idx, - .target_node_idx = target_node_idx, - .region = region, - } }, Content{ .flex_var = null }, region); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + // If we didn't find a valid node index, check if we should report an error + if (target_node_idx_opt) |target_node_idx| { + // Create the e_lookup_external expression with Import.Idx + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_external = .{ + .module_idx = import_idx, + .target_node_idx = target_node_idx, + .region = region, + } }, region); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; + } else { + // Check if the module is in module_envs - if not, the import failed + // and we shouldn't report a redundant "does not exist" error + const module_exists = if (self.module_envs) |envs_map| + envs_map.contains(exposed_info.module_name) + else + false; + + if (module_exists) { + // The exposed item doesn't actually exist in the module + // This can happen with qualified identifiers like `Try.blah` + // where `Try` is a valid type module but `blah` doesn't exist + return CanonicalizedExpr{ + .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .qualified_ident_does_not_exist = .{ + .ident = ident, + .region = region, + } }), + .free_vars = DataSpan.empty(), + }; + } + // Module doesn't exist, fall through to ident_not_in_scope error below + } } - // We did not find the ident in scope or as an exposed item + // Check if we're in a scope that allows forward references (associated blocks) + // Walk through scopes looking for one with associated_type_name set + const allows_forward_refs = blk: { + for (self.scopes.items) |*scope| { + if (scope.associated_type_name != null) break :blk true; + } + break :blk false; + }; + + if (allows_forward_refs) { + // Create a forward reference pattern and add it to the current scope + const forward_ref_pattern = Pattern{ + .assign = .{ + .ident = ident, + }, + }; + const pattern_idx = try self.env.addPattern(forward_ref_pattern, region); + + // Add to forward_references in the current scope + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Create the forward reference with an ArrayList for regions + var reference_regions = std.ArrayList(Region){}; + try reference_regions.append(self.env.gpa, region); + + const forward_ref: Scope.ForwardReference = .{ + .pattern_idx = pattern_idx, + .reference_regions = reference_regions, + }; + + try current_scope.forward_references.put(self.env.gpa, ident, forward_ref); + + // Also add to idents so subsequent lookups will find it + try current_scope.idents.put(self.env.gpa, ident, pattern_idx); + + // Return a lookup to this forward reference + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_local = .{ + .pattern_idx = pattern_idx, + } }, region); + + const free_vars_start = self.scratch_free_vars.top(); + try self.scratch_free_vars.append(pattern_idx); + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; + } + + // Check if this is a required identifier from the platform's `requires` clause + const requires_items = self.env.requires_types.items.items; + for (requires_items, 0..) |req, idx| { + if (req.ident == ident) { + // Found a required identifier - create a lookup expression for it + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_required = .{ + .requires_idx = ModuleEnv.RequiredType.SafeList.Idx.fromU32(@intCast(idx)), + } }, region); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; + } + } + + // We did not find the ident in scope or as an exposed item, and forward refs not allowed return CanonicalizedExpr{ .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .ident_not_in_scope = .{ .ident = ident, .region = region, } }), - .free_vars = null, + .free_vars = DataSpan.empty(), }; }, } @@ -1994,14 +4364,14 @@ pub fn canonicalizeExpr( .feature = feature, .region = region, } }), - .free_vars = null, + .free_vars = DataSpan.empty(), }; } }, .int => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); const token_text = self.parse_ir.resolve(e.token); - const parsed = types.Num.parseNumLiteralWithSuffix(token_text); + const parsed = types.parseNumeralWithSuffix(token_text); // Parse the integer value const is_negated = parsed.num_text[0] == '-'; @@ -2040,220 +4410,206 @@ pub fn canonicalizeExpr( const u128_val = parseIntWithUnderscores(u128, digit_part, int_base) catch { // Any number literal that is too large for u128 is invalid, regardless of whether it had a minus sign! const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; }; // If this had a minus sign, but negating it would result in a negative number // that would be too low to fit in i128, then this int literal is also invalid. if (is_negated and u128_val > min_i128_negated) { const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } - // Now we've confirmed that our int literal is one of these: - // * A signed integer that fits in i128 - // * An unsigned integer that fits in u128 - // - // We'll happily bitcast a u128 to i128 for storage (and bitcast it back later - // using its type information), but for negative numbers, we do need to actually - // negate them (branchlessly) if we skipped its minus sign earlier. - // - // This operation should never overflow i128, because we already would have errored out - // if the u128 portion was bigger than the lowest i128 without a minus sign. - // Special case: exactly i128 min already has the correct bit pattern when bitcast from u128, - // so if we try to negate it we'll get an overflow. We specifically *don't* negate that one. - const i128_val: i128 = if (is_negated) blk: { - if (u128_val == min_i128_negated) { - break :blk @as(i128, @bitCast(u128_val)); + // Determine the appropriate storage type + const int_value = blk: { + if (is_negated) { + // Negative: must be i128 (or smaller) + const i128_val = if (u128_val == min_i128_negated) + std.math.minInt(i128) // Special case for -2^127 + else + -@as(i128, @intCast(u128_val)); + break :blk CIR.IntValue{ + .bytes = @bitCast(i128_val), + .kind = .i128, + }; } else { - break :blk -@as(i128, @bitCast(u128_val)); - } - } else @as(i128, @bitCast(u128_val)); - - // Calculate requirements based on the value - // Special handling for minimum signed values (-128, -32768, etc.) - // These are special because they have a power-of-2 magnitude that fits exactly - // in their signed type. We report them as needing one less bit to make the - // standard "signed types have n-1 usable bits" logic work correctly. - if (parsed.suffix) |suffix| { - const type_content = blk: { - if (std.mem.eql(u8, suffix, "u8")) { - if (u128_val > std.math.maxInt(u8)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_u8 } }; - } else if (std.mem.eql(u8, suffix, "u16")) { - if (u128_val > std.math.maxInt(u16)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_u16 } }; - } else if (std.mem.eql(u8, suffix, "u32")) { - if (u128_val > std.math.maxInt(u32)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_u32 } }; - } else if (std.mem.eql(u8, suffix, "u64")) { - if (u128_val > std.math.maxInt(u64)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_u64 } }; - } else if (std.mem.eql(u8, suffix, "u128")) { - break :blk Content{ .structure = FlatType{ .num = Num.int_u128 } }; - } else if (std.mem.eql(u8, suffix, "i8")) { - if (i128_val < std.math.minInt(i8) or i128_val > std.math.maxInt(i8)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_i8 } }; - } else if (std.mem.eql(u8, suffix, "i16")) { - if (i128_val < std.math.minInt(i16) or i128_val > std.math.maxInt(i16)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_i16 } }; - } else if (std.mem.eql(u8, suffix, "i32")) { - if (i128_val < std.math.minInt(i32) or i128_val > std.math.maxInt(i32)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_i32 } }; - } else if (std.mem.eql(u8, suffix, "i64")) { - if (i128_val < std.math.minInt(i64) or i128_val > std.math.maxInt(i64)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_i64 } }; - } else if (std.mem.eql(u8, suffix, "i128")) { - break :blk Content{ .structure = FlatType{ .num = Num.int_i128 } }; + // Positive: could be i128 or u128 + if (u128_val > @as(u128, std.math.maxInt(i128))) { + // Too big for i128, keep as u128 + break :blk CIR.IntValue{ + .bytes = @bitCast(u128_val), + .kind = .u128, + }; } else { - break :blk null; + // Fits in i128 + break :blk CIR.IntValue{ + .bytes = @bitCast(@as(i128, @intCast(u128_val))), + .kind = .i128, + }; + } + } + }; + + // If a user provided a suffix, then we treat is as an type + // annotation to apply to the number + if (parsed.suffix) |suffix| { + // Capture the suffix, if provided + const num_suffix: CIR.NumKind = blk: { + if (std.mem.eql(u8, suffix, "u8")) { + break :blk .u8; + } else if (std.mem.eql(u8, suffix, "u16")) { + break :blk .u16; + } else if (std.mem.eql(u8, suffix, "u32")) { + break :blk .u32; + } else if (std.mem.eql(u8, suffix, "u64")) { + break :blk .u64; + } else if (std.mem.eql(u8, suffix, "u128")) { + break :blk .u128; + } else if (std.mem.eql(u8, suffix, "i8")) { + break :blk .i8; + } else if (std.mem.eql(u8, suffix, "i16")) { + break :blk .i16; + } else if (std.mem.eql(u8, suffix, "i32")) { + break :blk .i32; + } else if (std.mem.eql(u8, suffix, "i64")) { + break :blk .i64; + } else if (std.mem.eql(u8, suffix, "i128")) { + break :blk .i128; + } else if (std.mem.eql(u8, suffix, "f32")) { + break :blk .f32; + } else if (std.mem.eql(u8, suffix, "f64")) { + break :blk .f64; + } else if (std.mem.eql(u8, suffix, "dec")) { + break :blk .dec; + } else { + // Invalid numeric suffix - the suffix doesn't match any known type + const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region } }); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } }; - if (type_content) |content| { - const expr_idx = try self.env.addExprAndTypeVar( - .{ .e_int = .{ .value = .{ .bytes = @bitCast(i128_val), .kind = .i128 } } }, - content, - region, - ); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; - } + // Note that type-checking will ensure that the actual int value + // fits into the provided type + + const expr_idx = try self.env.addExpr( + .{ .e_num = .{ .value = int_value, .kind = num_suffix } }, + region, + ); + + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } - const is_negative_u1 = @as(u1, @intFromBool(is_negated)); - const is_power_of_2 = @as(u1, @intFromBool(u128_val != 0 and (u128_val & (u128_val - 1)) == 0)); - const is_minimum_signed = is_negative_u1 & is_power_of_2; - const adjusted_val = u128_val - is_minimum_signed; - - const requirements = types.Num.Int.Requirements{ - .sign_needed = is_negated, - .bits_needed = types.Num.Int.BitsNeeded.fromValue(adjusted_val), - }; - - const int_requirements = types.Num.IntRequirements{ - .sign_needed = requirements.sign_needed, - .bits_needed = @intCast(@intFromEnum(requirements.bits_needed)), - }; - - // For non-decimal integers (hex, binary, octal), use int_poly directly - // For decimal integers, use num_poly so they can be either Int or Frac - const is_non_decimal = int_base != DEFAULT_BASE; - - // Insert concrete type variable - const type_content = if (is_non_decimal) - Content{ .structure = .{ .num = .{ .int_unbound = int_requirements } } } - else - Content{ .structure = .{ .num = .{ .num_unbound = int_requirements } } }; - - const expr_idx = try self.env.addExprAndTypeVar( - CIR.Expr{ .e_int = .{ .value = CIR.IntValue{ - .bytes = @bitCast(i128_val), - .kind = .i128, - } } }, - type_content, + // Insert concrete expr + // All integer literals (regardless of base) are treated as num_unbound + // so they can unify with both Int and Frac types + const expr_idx = try self.env.addExpr( + CIR.Expr{ .e_num = .{ + .value = int_value, + .kind = .num_unbound, + } }, region, ); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; }, .frac => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); // Resolve to a string slice from the source const token_text = self.parse_ir.resolve(e.token); - const parsed_num = types.Num.parseNumLiteralWithSuffix(token_text); + const parsed_num = types.parseNumeralWithSuffix(token_text); if (parsed_num.suffix) |suffix| { const f64_val = std.fmt.parseFloat(f64, parsed_num.num_text) catch { const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; }; if (std.mem.eql(u8, suffix, "f32")) { - if (!fitsInF32(f64_val)) { + if (!CIR.fitsInF32(f64_val)) { const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } - const expr_idx = try self.env.addExprAndTypeVar( - .{ .e_frac_f32 = .{ .value = @floatCast(f64_val) } }, - .{ .structure = FlatType{ .num = .{ .frac_precision = .f32 } } }, + const expr_idx = try self.env.addExpr( + .{ .e_frac_f32 = .{ + .value = @floatCast(f64_val), + .has_suffix = true, + } }, region, ); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } else if (std.mem.eql(u8, suffix, "f64")) { - const expr_idx = try self.env.addExprAndTypeVar( - .{ .e_frac_f64 = .{ .value = f64_val } }, - .{ .structure = FlatType{ .num = .{ .frac_precision = .f64 } } }, + const expr_idx = try self.env.addExpr( + .{ .e_frac_f64 = .{ + .value = f64_val, + .has_suffix = true, + } }, region, ); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } else if (std.mem.eql(u8, suffix, "dec")) { - if (!fitsInDec(f64_val)) { + if (!CIR.fitsInDec(f64_val)) { const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } const dec_val = RocDec.fromF64(f64_val) orelse { const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; }; - const expr_idx = try self.env.addExprAndTypeVar( - .{ .e_frac_dec = .{ .value = dec_val } }, - .{ .structure = FlatType{ .num = .{ .frac_precision = .dec } } }, + const expr_idx = try self.env.addExpr( + .{ .e_dec = .{ + .value = dec_val, + .has_suffix = true, + } }, region, ); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } } const parsed = parseFracLiteral(token_text) catch |err| switch (err) { - error.InvalidNumLiteral => { + error.InvalidNumeral => { const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region, } }); return CanonicalizedExpr{ .idx = expr_idx, - .free_vars = null, + .free_vars = DataSpan.empty(), }; }, }; - // Parse the literal first to get requirements - const requirements = switch (parsed) { - .small => |small_info| small_info.requirements, - .dec => |dec_info| dec_info.requirements, - .f64 => |f64_info| f64_info.requirements, - }; - - const frac_requirements = types.Num.FracRequirements{ - .fits_in_f32 = requirements.fits_in_f32, - .fits_in_dec = requirements.fits_in_dec, - }; - const cir_expr = switch (parsed) { .small => |small_info| CIR.Expr{ .e_dec_small = .{ - .numerator = small_info.numerator, - .denominator_power_of_ten = small_info.denominator_power_of_ten, + .value = .{ + .numerator = small_info.numerator, + .denominator_power_of_ten = small_info.denominator_power_of_ten, + }, + .has_suffix = false, }, }, .dec => |dec_info| CIR.Expr{ - .e_frac_dec = .{ + .e_dec = .{ .value = dec_info.value, + .has_suffix = false, }, }, .f64 => |f64_info| CIR.Expr{ .e_frac_f64 = .{ .value = f64_info.value, + .has_suffix = false, }, }, }; - const expr_idx = try self.env.addExprAndTypeVar(cir_expr, Content{ .structure = .{ .num = .{ .frac_unbound = frac_requirements } } }, region); + const expr_idx = try self.env.addExpr(cir_expr, region); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; }, .single_quote => |e| { const expr_idx = try self.canonicalizeSingleQuote(e.region, e.token, Expr.Idx) orelse return null; - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; }, .string => |e| { return try self.canonicalizeStringLike(e, false); @@ -2268,11 +4624,11 @@ pub fn canonicalizeExpr( const items_slice = self.parse_ir.store.exprSlice(e.items); if (items_slice.len == 0) { // Empty list - use e_empty_list - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_empty_list = .{}, - }, Content{ .structure = .list_unbound }, region); + }, region); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } // Mark the start of scratch expressions for the list @@ -2293,38 +4649,32 @@ pub fn canonicalizeExpr( // If all elements failed to canonicalize, treat as empty list if (elems_span.span.len == 0) { // All elements failed to canonicalize - create empty list - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_empty_list = .{}, - }, Content{ .structure = .list_unbound }, region); + }, region); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } - // Initialize the list's type variable to its first element's CIR Index - // (later steps will unify that type with the other elems' types) - const first_elem_idx = self.env.store.sliceExpr(elems_span)[0]; - const elem_type_var = @as(TypeVar, @enumFromInt(@intFromEnum(first_elem_idx))); - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ - .e_list = .{ - .elem_var = elem_type_var, - .elems = elems_span, - }, - }, Content{ .structure = .{ .list = elem_type_var } }, region); + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_list = .{ .elems = elems_span }, + }, region); - const free_vars_slice = self.scratch_free_vars.slice(free_vars_start, self.scratch_free_vars.top()); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_slice.len > 0) free_vars_slice else null }; + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; }, .tag => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); return self.canonicalizeTagExpr(e, null, region); }, - .string_part => |_| { + .string_part => |sp| { + const region = self.parse_ir.tokenizedRegionToRegion(sp.region); const feature = try self.env.insertString("canonicalize string_part expression"); const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .not_implemented = .{ .feature = feature, - .region = Region.zero(), + .region = region, } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; }, .tuple => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); @@ -2338,7 +4688,7 @@ pub fn canonicalizeExpr( const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .empty_tuple = .{ .region = body_region }, }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } else if (items_slice.len == 1) { // 1-elem tuple == parenthesized expr @@ -2365,7 +4715,7 @@ pub fn canonicalizeExpr( .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .tuple_elem_not_canonicalized = .{ .region = body_region }, }), - .free_vars = null, + .free_vars = DataSpan.empty(), }; } }; @@ -2373,28 +4723,18 @@ pub fn canonicalizeExpr( try self.env.store.addScratchExpr(item_expr_idx.get_idx()); } - // Since expr idx map 1-to-1 to variables, we can get cast the slice - // of scratch expr idx and cast them to vars - const elems_var_range = try self.env.types.appendVars( - @ptrCast(@alignCast( - self.env.store.scratch_exprs.slice(scratch_top, self.env.store.scratchExprTop()), - )), - ); - // Create span of the new scratch expressions const elems_span = try self.env.store.exprSpanFrom(scratch_top); // Then insert the tuple expr - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_tuple = .{ .elems = elems_span, }, - }, Content{ .structure = FlatType{ - .tuple = types.Tuple{ .elems = elems_var_range }, - } }, region); + }, region); - const free_vars_slice = self.scratch_free_vars.slice(free_vars_start, self.scratch_free_vars.top()); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_slice.len > 0) free_vars_slice else null }; + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; } }, .record => |e| { @@ -2411,15 +4751,15 @@ pub fn canonicalizeExpr( const fields_slice = self.parse_ir.store.recordFieldSlice(e.fields); if (fields_slice.len == 0) { - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_empty_record = .{}, - }, Content{ .structure = .empty_record }, region); + }, region); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } // Mark the start of scratch record fields for the record - const scratch_top = self.env.store.scratch_record_fields.top(); + const scratch_top = self.env.store.scratch.?.record_fields.top(); // Track field names to detect duplicates const seen_fields_top = self.scratch_seen_record_fields.top(); @@ -2453,22 +4793,21 @@ pub fn canonicalizeExpr( if (!found_duplicate) { // First occurrence of this field name - try self.scratch_seen_record_fields.append(self.env.gpa, SeenRecordField{ + try self.scratch_seen_record_fields.append(SeenRecordField{ .ident = field_name_ident, .region = field_name_region, }); // Only canonicalize and include non-duplicate fields if (try self.canonicalizeRecordField(field)) |can_field_idx| { - try self.env.store.scratch_record_fields.append(self.env.gpa, can_field_idx); + try self.env.store.scratch.?.record_fields.append(can_field_idx); } - } else { - // TODO: Add diagnostic on duplicate record field } + // Duplicate fields are skipped - diagnostic already emitted above } else { // Field name couldn't be resolved, still try to canonicalize if (try self.canonicalizeRecordField(field)) |can_field_idx| { - try self.env.store.scratch_record_fields.append(self.env.gpa, can_field_idx); + try self.env.store.scratch.?.record_fields.append(can_field_idx); } } } @@ -2478,38 +4817,16 @@ pub fn canonicalizeExpr( // Create span of the new scratch record fields const fields_span = try self.env.store.recordFieldSpanFrom(scratch_top); - // Create fresh type variables for each record field - // The type checker will unify these with the field expression types - const cir_fields = self.env.store.sliceRecordFields(fields_span); - // Create fresh type variables for each field - const record_fields_top = self.scratch_record_fields.top(); - - for (cir_fields) |cir_field_idx| { - const cir_field = self.env.store.getRecordField(cir_field_idx); - try self.scratch_record_fields.append(self.env.gpa, types.RecordField{ - .name = cir_field.name, - .var_ = @enumFromInt(@intFromEnum(cir_field.value)), - }); - } - - // Create the record type structure - const type_fields_range = try self.env.types.appendRecordFields( - self.scratch_record_fields.sliceFromStart(record_fields_top), - ); - - // Shink the scratch array to it's original size - self.scratch_record_fields.clearFrom(record_fields_top); - - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_record = .{ .fields = fields_span, .ext = ext_expr, }, - }, Content{ .structure = .{ .record_unbound = type_fields_range } }, region); + }, region); - const free_vars_slice = self.scratch_free_vars.slice(free_vars_start, self.scratch_free_vars.top()); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_slice.len > 0) free_vars_slice else null }; + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; }, .lambda => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); @@ -2522,81 +4839,85 @@ pub fn canonicalizeExpr( try self.scopeEnter(self.env.gpa, true); // true = is_function_boundary defer self.scopeExit(self.env.gpa) catch {}; - // args - const gpa = self.env.gpa; - const args_start = self.env.store.scratch_patterns.top(); + // Canonicalize the lambda args + const args_start = self.env.store.scratch.?.patterns.top(); for (self.parse_ir.store.patternSlice(e.args)) |arg_pattern_idx| { if (try self.canonicalizePattern(arg_pattern_idx)) |pattern_idx| { - try self.env.store.scratch_patterns.append(gpa, pattern_idx); + try self.env.store.scratch.?.patterns.append(pattern_idx); } else { const arg = self.parse_ir.store.getPattern(arg_pattern_idx); const arg_region = self.parse_ir.tokenizedRegionToRegion(arg.to_tokenized_region()); const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .pattern_arg_invalid = .{ .region = arg_region, } }); - try self.env.store.scratch_patterns.append(gpa, malformed_idx); + try self.env.store.scratch.?.patterns.append(malformed_idx); } } const args_span = try self.env.store.patternSpanFrom(args_start); - // body (this will detect and record captures) - const body_free_vars_start = self.scratch_free_vars.top(); - const can_body = try self.canonicalizeExpr(e.body) orelse { - self.scratch_free_vars.clearFrom(body_free_vars_start); - const ast_body = self.parse_ir.store.getExpr(e.body); - const body_region = self.parse_ir.tokenizedRegionToRegion(ast_body.to_tokenized_region()); - const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ - .lambda_body_not_canonicalized = .{ .region = body_region }, - }); - return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = null }; - }; + // Define the set of captures + const captures_top = self.scratch_captures.top(); + defer self.scratch_captures.clearFrom(captures_top); - // Determine captures: free variables in body minus variables bound by args - var bound_vars = std.AutoHashMapUnmanaged(Pattern.Idx, void){}; - defer bound_vars.deinit(self.env.gpa); - for (self.env.store.slicePatterns(args_span)) |arg_pat_idx| { - try self.collectBoundVars(arg_pat_idx, &bound_vars); - } + // Canonicalize the lambda body + const body_idx = blk: { + const body_free_vars_start = self.scratch_free_vars.top(); + defer self.scratch_free_vars.clearFrom(body_free_vars_start); - var captures_set = std.AutoHashMapUnmanaged(Pattern.Idx, void){}; - defer captures_set.deinit(self.env.gpa); + const can_body = try self.canonicalizeExpr(e.body) orelse { + const ast_body = self.parse_ir.store.getExpr(e.body); + const body_region = self.parse_ir.tokenizedRegionToRegion(ast_body.to_tokenized_region()); + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ + .lambda_body_not_canonicalized = .{ .region = body_region }, + }); + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; + }; - const body_free_vars_slice = can_body.free_vars orelse &.{}; - for (body_free_vars_slice) |fv| { - if (!bound_vars.contains(fv)) { - try captures_set.put(self.env.gpa, fv, {}); + // Determine captures: free variables in body minus variables bound by args + const bound_vars_top = self.scratch_bound_vars.top(); + defer self.scratch_bound_vars.clearFrom(bound_vars_top); + + for (self.env.store.slicePatterns(args_span)) |arg_pat_idx| { + try self.collectBoundVarsToScratch(arg_pat_idx); } - } - // Now that we have the captures, we can clear the free variables from the body - // from the scratch buffer. - self.scratch_free_vars.clearFrom(body_free_vars_start); + const body_free_vars_slice = self.scratch_free_vars.sliceFromSpan(can_body.free_vars); + var bound_vars_view = self.scratch_bound_vars.setViewFrom(bound_vars_top); + defer bound_vars_view.deinit(); + for (body_free_vars_slice) |fv| { + if (!self.scratch_captures.contains(fv) and !bound_vars_view.contains(fv)) { + try self.scratch_captures.append(fv); + } + } + + break :blk can_body.idx; + }; // Create the pure lambda expression first const lambda_expr = Expr{ .e_lambda = .{ .args = args_span, - .body = can_body.idx, + .body = body_idx, }, }; - const lambda_type_content = try self.env.types.mkFuncUnbound( - @ptrCast(self.env.store.slicePatterns(args_span)), - ModuleEnv.varFrom(can_body.idx), - ); - const lambda_idx = try self.env.addExprAndTypeVar(lambda_expr, lambda_type_content, region); + + const lambda_idx = try self.env.addExpr(lambda_expr, region); + + // Get a slice of the captured vars in the body + const captures_slice = self.scratch_captures.sliceFromStart(captures_top); // If there are no captures, this is a pure lambda. - // Otherwise, it's a closure. - if (captures_set.count() == 0) { - // A pure lambda has no free variables. - return CanonicalizedExpr{ .idx = lambda_idx, .free_vars = null }; + // A pure lambda has no free variables. + if (captures_slice.len == 0) { + return CanonicalizedExpr{ .idx = lambda_idx, .free_vars = DataSpan.empty() }; } + // Otherwise, it's a closure. + + // Copy the captures into the store const capture_info: Expr.Capture.Span = blk: { - const scratch_start = self.env.store.scratch_captures.top(); - var cap_it = captures_set.iterator(); - while (cap_it.next()) |entry| { - const pattern_idx = entry.key_ptr.*; + const scratch_start = self.env.store.scratch.?.captures.top(); + for (captures_slice) |pattern_idx| { const pattern = self.env.store.getPattern(pattern_idx); const name = switch (pattern) { .assign => |a| a.ident, @@ -2607,7 +4928,7 @@ pub fn canonicalizeExpr( .pattern_idx = pattern_idx, .scope_depth = 0, // This is now unused, but kept for struct compatibility. }; - const capture_idx = try self.env.addCaptureAndTypeVar(capture, types.Content{ .flex_var = null }, region); + const capture_idx = try self.env.addCapture(capture, region); try self.env.store.addScratchCapture(capture_idx); } @@ -2621,47 +4942,185 @@ pub fn canonicalizeExpr( .captures = capture_info, }, }; - // The type of the closure is the same as the type of the pure lambda - const expr_idx = try self.env.addExprAndTypeVar(closure_expr, lambda_type_content, region); + const expr_idx = try self.env.addExpr(closure_expr, region); // The free variables of the lambda are its captures. - // I need to add them to the global list and return a span. + // Copy the contiguous list to the backing array const lambda_free_vars_start = self.scratch_free_vars.top(); - var cap_it = captures_set.iterator(); - while (cap_it.next()) |entry| { - try self.scratch_free_vars.append(self.env.gpa, entry.key_ptr.*); + for (captures_slice) |pattern_idx| { + try self.scratch_free_vars.append(pattern_idx); } - const free_vars_slice = self.scratch_free_vars.slice(lambda_free_vars_start, self.scratch_free_vars.top()); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_slice.len > 0) free_vars_slice else null }; + const free_vars_span = self.scratch_free_vars.spanFrom(lambda_free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; }, - .record_updater => |_| { + .record_updater => |ru| { + const region = self.parse_ir.tokenizedRegionToRegion(ru.region); const feature = try self.env.insertString("canonicalize record_updater expression"); const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .not_implemented = .{ .feature = feature, - .region = Region.zero(), + .region = region, } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; }, .field_access => |field_access| { - // Try module-qualified lookup first (e.g., Json.utf8) + // Track free vars from receiver and arguments + const free_vars_start = self.scratch_free_vars.top(); + + // Try type var alias dispatch first (e.g., Thing.method() where Thing : thing) + if (try self.tryTypeVarAliasDispatch(field_access)) |expr_idx| { + // Type var alias dispatch doesn't have free vars directly + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; + } + + // Try module-qualified lookup next (e.g., Json.utf8) if (try self.tryModuleQualifiedLookup(field_access)) |expr_idx| { - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + // Module-qualified lookups don't have free vars (they reference external definitions) + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; } // Regular field access canonicalization + const expr_idx = (try self.canonicalizeRegularFieldAccess(field_access)) orelse return null; + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); return CanonicalizedExpr{ - .idx = (try self.canonicalizeRegularFieldAccess(field_access)) orelse return null, - .free_vars = null, + .idx = expr_idx, + .free_vars = free_vars_span, }; }, - .local_dispatch => |_| { - const feature = try self.env.insertString("canonicalize local_dispatch expression"); - const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .not_implemented = .{ - .feature = feature, - .region = Region.zero(), - } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + .local_dispatch => |local_dispatch| { + // Desugar `arg1->fn(arg2, arg3)` to `fn(arg1, arg2, arg3)` + // and `arg1->fn` to `fn(arg1)` + const region = self.parse_ir.tokenizedRegionToRegion(local_dispatch.region); + const free_vars_start = self.scratch_free_vars.top(); + + // Canonicalize the left expression (first argument) + const can_first_arg = try self.canonicalizeExpr(local_dispatch.left) orelse return null; + + // Get the right expression to determine the function and additional args + const right_expr = self.parse_ir.store.getExpr(local_dispatch.right); + + switch (right_expr) { + .apply => |apply| { + // Case: `arg1->fn(arg2, arg3)` - function call with additional args + // Check if this is a tag application + const ast_fn = self.parse_ir.store.getExpr(apply.@"fn"); + if (ast_fn == .tag) { + // Tag application: `arg1->Tag(arg2)` becomes `Tag(arg1, arg2)` + const tag_expr = ast_fn.tag; + const tag_name = self.parse_ir.tokens.resolveIdentifier(tag_expr.token) orelse { + // Parser should have validated this, but handle gracefully + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = region, + } }); + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; + }; + + // Build args: first_arg followed by apply.args + const scratch_top = self.env.store.scratchExprTop(); + try self.env.store.addScratchExpr(can_first_arg.idx); + + const additional_args = self.parse_ir.store.exprSlice(apply.args); + for (additional_args) |arg| { + if (try self.canonicalizeExpr(arg)) |can_arg| { + try self.env.store.addScratchExpr(can_arg.idx); + } + } + + const args_span = try self.env.store.exprSpanFrom(scratch_top); + + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_tag = .{ + .name = tag_name, + .args = args_span, + }, + }, region); + + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; + } + + // Normal function call + const can_fn_expr = try self.canonicalizeExpr(apply.@"fn") orelse return null; + + // Build args: first_arg followed by apply.args + const scratch_top = self.env.store.scratchExprTop(); + try self.env.store.addScratchExpr(can_first_arg.idx); + + const additional_args = self.parse_ir.store.exprSlice(apply.args); + for (additional_args) |arg| { + if (try self.canonicalizeExpr(arg)) |can_arg| { + try self.env.store.addScratchExpr(can_arg.idx); + } + } + + const args_span = try self.env.store.exprSpanFrom(scratch_top); + + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_call = .{ + .func = can_fn_expr.idx, + .args = args_span, + .called_via = CalledVia.apply, + }, + }, region); + + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; + }, + .ident, .tag => { + // Case: `arg1->fn` or `arg1->Tag` - simple function/tag call with single arg + if (right_expr == .tag) { + const tag_expr = right_expr.tag; + const tag_name = self.parse_ir.tokens.resolveIdentifier(tag_expr.token) orelse { + // Parser should have validated this, but handle gracefully + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = region, + } }); + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; + }; + + const scratch_top = self.env.store.scratchExprTop(); + try self.env.store.addScratchExpr(can_first_arg.idx); + const args_span = try self.env.store.exprSpanFrom(scratch_top); + + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_tag = .{ + .name = tag_name, + .args = args_span, + }, + }, region); + + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; + } + + // It's an ident + const can_fn_expr = try self.canonicalizeExpr(local_dispatch.right) orelse return null; + + const scratch_top = self.env.store.scratchExprTop(); + try self.env.store.addScratchExpr(can_first_arg.idx); + const args_span = try self.env.store.exprSpanFrom(scratch_top); + + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_call = .{ + .func = can_fn_expr.idx, + .args = args_span, + .called_via = CalledVia.apply, + }, + }, region); + + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; + }, + else => { + // Unexpected expression type on right side of arrow + const feature = try self.env.insertString("arrow with complex expression"); + const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .not_implemented = .{ + .feature = feature, + .region = region, + } }); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; + }, + } }, .bin_op => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); @@ -2686,12 +5145,18 @@ pub fn canonicalizeExpr( .OpGreaterThanOrEq => .ge, .OpEquals => .eq, .OpNotEquals => .ne, - .OpCaret => .pow, .OpDoubleSlash => .div_trunc, .OpAnd => .@"and", .OpOr => .@"or", - .OpPizza => .pipe_forward, - .OpDoubleQuestion => .null_coalesce, + // OpCaret (^), OpPizza (|>), OpDoubleQuestion (?) are not supported + .OpCaret, .OpPizza, .OpDoubleQuestion => { + const feature = try self.env.insertString("unsupported operator"); + const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .not_implemented = .{ + .feature = feature, + .region = region, + } }); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; + }, else => { // Unknown operator const feature = try self.env.insertString("binop"); @@ -2699,24 +5164,182 @@ pub fn canonicalizeExpr( .feature = feature, .region = region, } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; }, }; - const expr_idx = try self.env.addExprAndTypeVar(Expr{ + const expr_idx = try self.env.addExpr(Expr{ .e_binop = Expr.Binop.init(op, can_lhs.idx, can_rhs.idx), - }, Content{ .flex_var = null }, region); + }, region); - const free_vars_slice = self.scratch_free_vars.slice(free_vars_start, self.scratch_free_vars.top()); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_slice.len > 0) free_vars_slice else null }; + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; }, - .suffix_single_question => |_| { - const feature = try self.env.insertString("canonicalize suffix_single_question expression"); - const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .not_implemented = .{ - .feature = feature, - .region = Region.zero(), - } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + .suffix_single_question => |unary| { + // Desugar `expr?` into: + // match expr { + // Ok(#ok) => #ok, + // Err(#err) => return Err(#err), + // } + const region = self.parse_ir.tokenizedRegionToRegion(unary.region); + + const free_vars_start = self.scratch_free_vars.top(); + + // Canonicalize the inner expression (the expression before `?`) + const can_cond = try self.canonicalizeExpr(unary.expr) orelse return null; + + // Use pre-interned identifiers for the Ok/Err values and tag names + const ok_val_ident = self.env.idents.question_ok; + const err_val_ident = self.env.idents.question_err; + const ok_tag_ident = self.env.idents.ok; + const err_tag_ident = self.env.idents.err; + + // Mark the start of scratch match branches + const scratch_top = self.env.store.scratchMatchBranchTop(); + + // === Branch 1: Ok(#ok) => #ok === + { + // Enter a new scope for this branch + try self.scopeEnter(self.env.gpa, false); + defer self.scopeExit(self.env.gpa) catch {}; + + // Create the assign pattern for the Ok value + const ok_assign_pattern_idx = try self.env.addPattern(Pattern{ + .assign = .{ .ident = ok_val_ident }, + }, region); + + // Introduce the pattern into scope + _ = try self.scopeIntroduceInternal(self.env.gpa, .ident, ok_val_ident, ok_assign_pattern_idx, false, true); + + // Create pattern span for Ok tag argument + const ok_patterns_start = self.env.store.scratchPatternTop(); + try self.env.store.addScratchPattern(ok_assign_pattern_idx); + const ok_args_span = try self.env.store.patternSpanFrom(ok_patterns_start); + + // Create the Ok tag pattern: Ok(#ok) + const ok_tag_pattern_idx = try self.env.addPattern(Pattern{ + .applied_tag = .{ + .name = ok_tag_ident, + .args = ok_args_span, + }, + }, region); + + // Create branch pattern + const branch_pat_scratch_top = self.env.store.scratchMatchBranchPatternTop(); + const ok_branch_pattern_idx = try self.env.addMatchBranchPattern(Expr.Match.BranchPattern{ + .pattern = ok_tag_pattern_idx, + .degenerate = false, + }, region); + try self.env.store.addScratchMatchBranchPattern(ok_branch_pattern_idx); + const ok_branch_pat_span = try self.env.store.matchBranchPatternSpanFrom(branch_pat_scratch_top); + + // Create the branch body: lookup #ok + const ok_lookup_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_local = .{ + .pattern_idx = ok_assign_pattern_idx, + } }, region); + // Mark the pattern as used + try self.used_patterns.put(self.env.gpa, ok_assign_pattern_idx, {}); + + // Create the Ok branch + const ok_branch_idx = try self.env.addMatchBranch( + Expr.Match.Branch{ + .patterns = ok_branch_pat_span, + .value = ok_lookup_idx, + .guard = null, + .redundant = try self.env.types.fresh(), + }, + region, + ); + try self.env.store.addScratchMatchBranch(ok_branch_idx); + } + + // === Branch 2: Err(#err) => return Err(#err) === + { + // Enter a new scope for this branch + try self.scopeEnter(self.env.gpa, false); + defer self.scopeExit(self.env.gpa) catch {}; + + // Create the assign pattern for the Err value + const err_assign_pattern_idx = try self.env.addPattern(Pattern{ + .assign = .{ .ident = err_val_ident }, + }, region); + + // Introduce the pattern into scope + _ = try self.scopeIntroduceInternal(self.env.gpa, .ident, err_val_ident, err_assign_pattern_idx, false, true); + + // Create pattern span for Err tag argument + const err_patterns_start = self.env.store.scratchPatternTop(); + try self.env.store.addScratchPattern(err_assign_pattern_idx); + const err_args_span = try self.env.store.patternSpanFrom(err_patterns_start); + + // Create the Err tag pattern: Err(#err) + const err_tag_pattern_idx = try self.env.addPattern(Pattern{ + .applied_tag = .{ + .name = err_tag_ident, + .args = err_args_span, + }, + }, region); + + // Create branch pattern + const branch_pat_scratch_top = self.env.store.scratchMatchBranchPatternTop(); + const err_branch_pattern_idx = try self.env.addMatchBranchPattern(Expr.Match.BranchPattern{ + .pattern = err_tag_pattern_idx, + .degenerate = false, + }, region); + try self.env.store.addScratchMatchBranchPattern(err_branch_pattern_idx); + const err_branch_pat_span = try self.env.store.matchBranchPatternSpanFrom(branch_pat_scratch_top); + + // Create the branch body: return Err(#err) + // First, create lookup for #err + const err_lookup_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_local = .{ + .pattern_idx = err_assign_pattern_idx, + } }, region); + // Mark the pattern as used + try self.used_patterns.put(self.env.gpa, err_assign_pattern_idx, {}); + + // Create Err(#err) tag expression + const err_tag_args_start = self.env.store.scratchExprTop(); + try self.env.store.addScratchExpr(err_lookup_idx); + const err_tag_args_span = try self.env.store.exprSpanFrom(err_tag_args_start); + + const err_tag_expr_idx = try self.env.addExpr(CIR.Expr{ + .e_tag = .{ + .name = err_tag_ident, + .args = err_tag_args_span, + }, + }, region); + + // Create return Err(#err) expression + const return_expr_idx = try self.env.addExpr(CIR.Expr{ .e_return = .{ + .expr = err_tag_expr_idx, + } }, region); + + // Create the Err branch + const err_branch_idx = try self.env.addMatchBranch( + Expr.Match.Branch{ + .patterns = err_branch_pat_span, + .value = return_expr_idx, + .guard = null, + .redundant = try self.env.types.fresh(), + }, + region, + ); + try self.env.store.addScratchMatchBranch(err_branch_idx); + } + + // Create span from scratch branches + const branches_span = try self.env.store.matchBranchSpanFrom(scratch_top); + + // Create the match expression + const match_expr = Expr.Match{ + .cond = can_cond.idx, + .branches = branches_span, + .exhaustive = try self.env.types.fresh(), + }; + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_match = match_expr }, region); + + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; }, .unary_op => |unary| { const region = self.parse_ir.tokenizedRegionToRegion(unary.region); @@ -2728,9 +5351,9 @@ pub fn canonicalizeExpr( const can_operand = (try self.canonicalizeExpr(unary.expr)) orelse return null; // Create unary minus CIR expression - const expr_idx = try self.env.addExprAndTypeVar(Expr{ + const expr_idx = try self.env.addExpr(Expr{ .e_unary_minus = Expr.UnaryMinus.init(can_operand.idx), - }, Content{ .flex_var = null }, region); + }, region); return CanonicalizedExpr{ .idx = expr_idx, .free_vars = can_operand.free_vars }; }, @@ -2739,9 +5362,9 @@ pub fn canonicalizeExpr( const can_operand = (try self.canonicalizeExpr(unary.expr)) orelse return null; // Create unary not CIR expression - const expr_idx = try self.env.addExprAndTypeVar(Expr{ + const expr_idx = try self.env.addExpr(Expr{ .e_unary_not = Expr.UnaryNot.init(can_operand.idx), - }, Content{ .flex_var = null }, region); + }, region); return CanonicalizedExpr{ .idx = expr_idx, .free_vars = can_operand.free_vars }; }, @@ -2752,13 +5375,18 @@ pub fn canonicalizeExpr( .feature = feature, .region = region, } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; }, } }, .if_then_else => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); + // Use scratch_captures as intermediate buffer for collecting free vars + // This avoids capturing intermediate data from nested block canonicalization + const captures_top = self.scratch_captures.top(); + defer self.scratch_captures.clearFrom(captures_top); + const free_vars_start = self.scratch_free_vars.top(); // Start collecting if-branches @@ -2776,24 +5404,40 @@ pub fn canonicalizeExpr( .if_condition_not_canonicalized = .{ .region = cond_region }, }); // In case of error, we can't continue, so we just return a malformed expression for the whole if-else chain - return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; }; + // Collect free variables from the condition into scratch_captures + const cond_free_vars_slice = self.scratch_free_vars.sliceFromSpan(can_cond.free_vars); + for (cond_free_vars_slice) |fv| { + if (!self.scratch_captures.contains(fv)) { + try self.scratch_captures.append(fv); + } + } + const can_then = try self.canonicalizeExpr(current_if.then) orelse { const ast_then = self.parse_ir.store.getExpr(current_if.then); const then_region = self.parse_ir.tokenizedRegionToRegion(ast_then.to_tokenized_region()); const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .if_then_not_canonicalized = .{ .region = then_region }, }); - return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; }; + // Collect free variables from the then-branch into scratch_captures + const then_free_vars_slice = self.scratch_free_vars.sliceFromSpan(can_then.free_vars); + for (then_free_vars_slice) |fv| { + if (!self.scratch_captures.contains(fv)) { + try self.scratch_captures.append(fv); + } + } + // Add this condition/then pair as an if-branch const if_branch = Expr.IfBranch{ .cond = can_cond.idx, .body = can_then.idx, }; - const if_branch_idx = try self.env.addIfBranchAndTypeVar(if_branch, Content{ .flex_var = null }, self.parse_ir.tokenizedRegionToRegion(current_if.region)); + const if_branch_idx = try self.env.addIfBranch(if_branch, self.parse_ir.tokenizedRegionToRegion(current_if.region)); try self.env.store.addScratchIfBranch(if_branch_idx); // Check if the else clause is another if-then-else @@ -2807,8 +5451,17 @@ pub fn canonicalizeExpr( const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .if_else_not_canonicalized = .{ .region = else_region }, }); - return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; }; + + // Collect free variables from the else-branch into scratch_captures + const else_free_vars_slice = self.scratch_free_vars.sliceFromSpan(can_else.free_vars); + for (else_free_vars_slice) |fv| { + if (!self.scratch_captures.contains(fv)) { + try self.scratch_captures.append(fv); + } + } + final_else = can_else.idx; break; } @@ -2821,21 +5474,116 @@ pub fn canonicalizeExpr( std.debug.assert(branches.len > 0); // Create the if expression with flex var initially - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_if = .{ .branches = branches_span, .final_else = final_else, }, - }, Content{ .flex_var = null }, region); + }, region); - // Immediately redirect the if expression's type variable to the first branch's body - const first_branch = self.env.store.getIfBranch(branches[0]); - const first_branch_type_var = @as(TypeVar, @enumFromInt(@intFromEnum(first_branch.body))); - const expr_var = @as(TypeVar, @enumFromInt(@intFromEnum(expr_idx))); - try self.env.types.setVarRedirect(expr_var, first_branch_type_var); + // Clear intermediate data from scratch_free_vars + self.scratch_free_vars.clearFrom(free_vars_start); - const free_vars_slice = self.scratch_free_vars.slice(free_vars_start, self.scratch_free_vars.top()); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_slice.len > 0) free_vars_slice else null }; + // Copy collected free vars from scratch_captures to scratch_free_vars + const if_free_vars_start = self.scratch_free_vars.top(); + const captures_slice = self.scratch_captures.sliceFromStart(captures_top); + for (captures_slice) |fv| { + try self.scratch_free_vars.append(fv); + } + const free_vars_span = self.scratch_free_vars.spanFrom(if_free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; + }, + .if_without_else => |e| { + // Statement form: if without else + const region = self.parse_ir.tokenizedRegionToRegion(e.region); + + // Check if we're in expression context (e.g., assignment, function call) + // If so, emit error explaining that if-expressions need else + if (!self.in_statement_position) { + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ + .if_expr_without_else = .{ .region = region }, + }); + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; + } + + // Desugar to if-then-else with empty record {} as the final else + // Type checking will ensure the then-branch also has type {} + + // Use scratch_captures as intermediate buffer for collecting free vars + // This avoids capturing intermediate data from nested block canonicalization + const captures_top = self.scratch_captures.top(); + defer self.scratch_captures.clearFrom(captures_top); + + const free_vars_start = self.scratch_free_vars.top(); + + // Canonicalize condition + const can_cond = try self.canonicalizeExpr(e.condition) orelse { + const ast_cond = self.parse_ir.store.getExpr(e.condition); + const cond_region = self.parse_ir.tokenizedRegionToRegion(ast_cond.to_tokenized_region()); + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ + .if_condition_not_canonicalized = .{ .region = cond_region }, + }); + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; + }; + + // Collect free variables from the condition into scratch_captures + const cond_free_vars_slice = self.scratch_free_vars.sliceFromSpan(can_cond.free_vars); + for (cond_free_vars_slice) |fv| { + if (!self.scratch_captures.contains(fv)) { + try self.scratch_captures.append(fv); + } + } + + // Canonicalize then branch + const can_then = try self.canonicalizeExpr(e.then) orelse { + const ast_then = self.parse_ir.store.getExpr(e.then); + const then_region = self.parse_ir.tokenizedRegionToRegion(ast_then.to_tokenized_region()); + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ + .if_then_not_canonicalized = .{ .region = then_region }, + }); + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; + }; + + // Collect free variables from the then-branch into scratch_captures + const then_free_vars_slice = self.scratch_free_vars.sliceFromSpan(can_then.free_vars); + for (then_free_vars_slice) |fv| { + if (!self.scratch_captures.contains(fv)) { + try self.scratch_captures.append(fv); + } + } + + // Create an empty record {} as the implicit else + const empty_record_idx = try self.env.addExpr(CIR.Expr{ .e_empty_record = .{} }, region); + + // Create single if branch + const scratch_top = self.env.store.scratchIfBranchTop(); + const if_branch = Expr.IfBranch{ + .cond = can_cond.idx, + .body = can_then.idx, + }; + const if_branch_idx = try self.env.addIfBranch(if_branch, region); + try self.env.store.addScratchIfBranch(if_branch_idx); + const branches_span = try self.env.store.ifBranchSpanFrom(scratch_top); + + // Create if expression with empty record as final else + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_if = .{ + .branches = branches_span, + .final_else = empty_record_idx, + }, + }, region); + + // Clear intermediate data from scratch_free_vars + self.scratch_free_vars.clearFrom(free_vars_start); + + // Copy collected free vars from scratch_captures to scratch_free_vars + const if_free_vars_start = self.scratch_free_vars.top(); + const captures_slice = self.scratch_captures.sliceFromStart(captures_top); + for (captures_slice) |fv| { + try self.scratch_free_vars.append(fv); + } + const free_vars_span = self.scratch_free_vars.spanFrom(if_free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; }, .match => |m| { const region = self.parse_ir.tokenizedRegionToRegion(m.region); @@ -2884,10 +5632,10 @@ pub fn canonicalizeExpr( } }; - const branch_pattern_idx = try self.env.addMatchBranchPatternAndTypeVar(Expr.Match.BranchPattern{ + const branch_pattern_idx = try self.env.addMatchBranchPattern(Expr.Match.BranchPattern{ .pattern = pattern_idx, .degenerate = false, - }, Content{ .flex_var = null }, alt_pattern_region); + }, alt_pattern_region); try self.env.store.addScratchMatchBranchPattern(branch_pattern_idx); } }, @@ -2904,10 +5652,10 @@ pub fn canonicalizeExpr( break :blk malformed_idx; } }; - const branch_pattern_idx = try self.env.addMatchBranchPatternAndTypeVar(Expr.Match.BranchPattern{ + const branch_pattern_idx = try self.env.addMatchBranchPattern(Expr.Match.BranchPattern{ .pattern = pattern_idx, .degenerate = false, - }, Content{ .flex_var = null }, pattern_region); + }, pattern_region); try self.env.store.addScratchMatchBranchPattern(branch_pattern_idx); }, } @@ -2916,6 +5664,17 @@ pub fn canonicalizeExpr( // Get the pattern span const branch_pat_span = try self.env.store.matchBranchPatternSpanFrom(branch_pat_scratch_top); + // Collect variables bound by the branch pattern(s) + const branch_bound_vars_top = self.scratch_bound_vars.top(); + defer self.scratch_bound_vars.clearFrom(branch_bound_vars_top); + for (self.env.store.sliceMatchBranchPatterns(branch_pat_span)) |branch_pat_idx| { + const branch_pat = self.env.store.getMatchBranchPattern(branch_pat_idx); + try self.collectBoundVarsToScratch(branch_pat.pattern); + } + + // Save position before canonicalizing body so we can filter pattern-bound vars + const body_free_vars_start = self.scratch_free_vars.top(); + // Canonicalize the branch's body const can_body = try self.canonicalizeExpr(ast_branch.body) orelse { const body = self.parse_ir.store.getExpr(ast_branch.body); @@ -2923,23 +5682,36 @@ pub fn canonicalizeExpr( const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ .region = body_region, } }); - return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; }; const value_idx = can_body.idx; - // Get the body region from the AST node - const body = self.parse_ir.store.getExpr(ast_branch.body); - const body_region = self.parse_ir.tokenizedRegionToRegion(body.to_tokenized_region()); + // Filter out pattern-bound variables from the body's free_vars + // Only truly free variables (not bound by this branch's pattern) should + // propagate up to the match expression's free_vars + if (can_body.free_vars.len > 0) { + // Copy the free vars we need to filter + const body_free_vars_slice = self.scratch_free_vars.sliceFromSpan(can_body.free_vars); + // Clear back to before body canonicalization + self.scratch_free_vars.clearFrom(body_free_vars_start); + // Re-add only filtered vars (not bound by branch patterns) + var bound_vars_view = self.scratch_bound_vars.setViewFrom(branch_bound_vars_top); + defer bound_vars_view.deinit(); + for (body_free_vars_slice) |fv| { + if (!bound_vars_view.contains(fv)) { + try self.scratch_free_vars.append(fv); + } + } + } - const branch_idx = try self.env.addMatchBranchAndTypeVar( + const branch_idx = try self.env.addMatchBranch( Expr.Match.Branch{ .patterns = branch_pat_span, .value = value_idx, .guard = null, - .redundant = @enumFromInt(0), // TODO + .redundant = try self.env.types.fresh(), }, - Content{ .flex_var = null }, - body_region, + region, ); // Set the branch var @@ -2957,22 +5729,12 @@ pub fn canonicalizeExpr( const match_expr = Expr.Match{ .cond = can_cond.idx, .branches = branches_span, - .exhaustive = @enumFromInt(0), // Will be set during type checking + .exhaustive = try self.env.types.fresh(), }; + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_match = match_expr }, region); - // Create initial content for the match expression - const initial_content = if (mb_branch_var) |_| Content{ .flex_var = null } else Content{ .err = {} }; - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ .e_match = match_expr }, initial_content, region); - - // If there is at least 1 branch, then set the root expr to redirect - // to the type of the match branch - const expr_var = @as(TypeVar, @enumFromInt(@intFromEnum(expr_idx))); - if (mb_branch_var) |branch_var| { - try self.env.types.setVarRedirect(expr_var, branch_var); - } - - const free_vars_slice = self.scratch_free_vars.slice(free_vars_start, self.scratch_free_vars.top()); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = if (free_vars_slice.len > 0) free_vars_slice else null }; + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = free_vars_span }; }, .dbg => |d| { // Debug expression - canonicalize the inner expression @@ -2980,191 +5742,45 @@ pub fn canonicalizeExpr( const can_inner = try self.canonicalizeExpr(d.expr) orelse return null; // Create debug expression - const dbg_expr = try self.env.addExprAndTypeVar(Expr{ .e_dbg = .{ + const dbg_expr = try self.env.addExpr(Expr{ .e_dbg = .{ .expr = can_inner.idx, - } }, Content{ .flex_var = null }, region); + } }, region); return CanonicalizedExpr{ .idx = dbg_expr, .free_vars = can_inner.free_vars }; }, - .record_builder => |_| { + .record_builder => |rb| { + const region = self.parse_ir.tokenizedRegionToRegion(rb.region); const feature = try self.env.insertString("canonicalize record_builder expression"); const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .not_implemented = .{ .feature = feature, - .region = Region.zero(), + .region = region, } }); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; }, .ellipsis => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); - const ellipsis_expr = try self.env.addExprAndTypeVar(Expr{ .e_ellipsis = .{} }, Content{ .flex_var = null }, region); - return CanonicalizedExpr{ .idx = ellipsis_expr, .free_vars = null }; + const ellipsis_expr = try self.env.addExpr(Expr{ .e_ellipsis = .{} }, region); + return CanonicalizedExpr{ .idx = ellipsis_expr, .free_vars = DataSpan.empty() }; }, .block => |e| { - const region = self.parse_ir.tokenizedRegionToRegion(e.region); - - var last_type_anno: ?StmtTypeAnno = null; - - // Blocks don't introduce function boundaries, but may contain var statements - try self.scopeEnter(self.env.gpa, false); // false = not a function boundary - defer self.scopeExit(self.env.gpa) catch {}; - - // Keep track of the start position for statements - const stmt_start = self.env.store.scratch_statements.top(); - - // TODO Use a temporary scratch space for the block's free variables - // - // I apologize for leaving these AutoHashMapUnmanaged's here ... but it's a workaround - // to land a working closure capture implementation, and we can optimize this later. Forgive me. - var bound_vars = std.AutoHashMapUnmanaged(Pattern.Idx, void){}; - defer bound_vars.deinit(self.env.gpa); - - var captures = std.AutoHashMapUnmanaged(Pattern.Idx, void){}; - defer captures.deinit(self.env.gpa); - - // Canonicalize all statements in the block - const statements = self.parse_ir.store.statementSlice(e.statements); - var last_expr: ?CanonicalizedExpr = null; - - for (statements, 0..) |stmt_idx, i| { - // Check if this is the last statement and if it's an expression - const is_last = (i == statements.len - 1); - const stmt = self.parse_ir.store.getStatement(stmt_idx); - - if (is_last and (stmt == .expr or stmt == .dbg or stmt == .@"return" or stmt == .crash)) { - // For the last expression or debug statement, canonicalize it directly as the final expression - // without adding it as a statement - switch (stmt) { - .expr => |expr_stmt| last_expr = try self.canonicalizeExprOrMalformed(expr_stmt.expr), - .dbg => |dbg_stmt| { - // For final debug statements, canonicalize as debug expression - const debug_region = self.parse_ir.tokenizedRegionToRegion(dbg_stmt.region); - const inner_expr = try self.canonicalizeExprOrMalformed(dbg_stmt.expr); - - // Create debug expression - const dbg_expr = try self.env.addExprAndTypeVarRedirect(Expr{ .e_dbg = .{ - .expr = inner_expr.idx, - } }, ModuleEnv.varFrom(inner_expr.idx), debug_region); - last_expr = CanonicalizedExpr{ .idx = dbg_expr, .free_vars = inner_expr.free_vars }; - }, - .@"return" => |return_stmt| last_expr = try self.canonicalizeExprOrMalformed(return_stmt.expr), - .crash => |crash_stmt| { - // For final debug statements, canonicalize as debug expression - const crash_region = self.parse_ir.tokenizedRegionToRegion(crash_stmt.region); - - // Create crash expression - // Extract string content from the crash expression or create malformed if not string - const crash_expr = blk: { - const msg_expr = self.parse_ir.store.getExpr(crash_stmt.expr); - switch (msg_expr) { - .string => |s| { - // For string literals, we need to extract the actual string parts - const parts = self.parse_ir.store.exprSlice(s.parts); - if (parts.len > 0) { - const first_part = self.parse_ir.store.getExpr(parts[0]); - if (first_part == .string_part) { - const part_text = self.parse_ir.resolve(first_part.string_part.token); - break :blk try self.env.addExprAndTypeVar(Expr{ .e_crash = .{ - .msg = try self.env.insertString(part_text), - } }, .{ .flex_var = null }, crash_region); - } - } - // Fall back to default if we can't extract - break :blk try self.env.addExprAndTypeVar(Expr{ .e_crash = .{ - .msg = try self.env.insertString("crash"), - } }, .{ .flex_var = null }, crash_region); - }, - else => { - // For non-string expressions, create a malformed expression - break :blk try self.env.pushMalformed(Expr.Idx, Diagnostic{ .crash_expects_string = .{ - .region = region, - } }); - }, - } - }; - - last_expr = CanonicalizedExpr{ .idx = crash_expr, .free_vars = null }; - }, - else => unreachable, - } - } else { - // This is a regular statement within the block - const can_stmt_result = try self.canonicalizeStatement(stmt_idx, &last_type_anno); - switch (can_stmt_result) { - .import_stmt => { - // After we process import statements, there's no - // need to include then in the canonicalize IR - }, - .stmt => |can_stmt| { - try self.env.store.addScratchStatement(can_stmt.idx); - - const cir_stmt = self.env.store.getStatement(can_stmt.idx); - switch (cir_stmt) { - .s_decl => |decl| try self.collectBoundVars(decl.pattern, &bound_vars), - .s_var => |var_stmt| try self.collectBoundVars(var_stmt.pattern_idx, &bound_vars), - else => {}, - } - - // Collect free vars from the statement into the block's scratch space - if (can_stmt.free_vars) |fvs| { - for (fvs) |fv| { - if (!bound_vars.contains(fv)) { - try captures.put(self.env.gpa, fv, {}); - } - } - } - }, - } - } - } - - // Determine the final expression - const final_expr = if (last_expr) |can_expr| can_expr else blk: { - // Empty block - create empty record - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ - .e_empty_record = .{}, - }, Content{ .structure = .empty_record }, region); - break :blk CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; - }; - const final_expr_var = @as(TypeVar, @enumFromInt(@intFromEnum(final_expr.idx))); - - // Add free vars from the final expression to the block's scratch space - if (final_expr.free_vars) |fvs| { - for (fvs) |fv| { - if (!bound_vars.contains(fv)) { - try captures.put(self.env.gpa, fv, {}); - } - } - } - - // Add the actual free variables (captures) to the parent's scratch space - const captures_start = self.scratch_free_vars.top(); - var cap_it = captures.iterator(); - while (cap_it.next()) |entry| { - try self.scratch_free_vars.append(self.env.gpa, entry.key_ptr.*); - } - const captures_slice = self.scratch_free_vars.slice(captures_start, self.scratch_free_vars.top()); - - // Create statement span - const stmt_span = try self.env.store.statementSpanFrom(stmt_start); - - // Create and return block expression - const block_expr = CIR.Expr{ - .e_block = .{ - .stmts = stmt_span, - .final_expr = final_expr.idx, - }, - }; - const block_idx = try self.env.addExprAndTypeVar(block_expr, Content{ .flex_var = null }, region); - const block_var = @as(TypeVar, @enumFromInt(@intFromEnum(block_idx))); - - // Set the root block expr to redirect to the final expr var - try self.env.types.setVarRedirect(block_var, final_expr_var); - - return CanonicalizedExpr{ .idx = block_idx, .free_vars = if (captures_slice.len > 0) captures_slice else null }; + return try self.canonicalizeBlock(e); }, - .malformed => |malformed| { + .for_expr => |for_expr| { + const region = self.parse_ir.tokenizedRegionToRegion(for_expr.region); + const result = try self.canonicalizeForLoop(for_expr.patt, for_expr.expr, for_expr.body); + + const for_expr_idx = try self.env.addExpr(Expr{ + .e_for = .{ + .patt = result.patt, + .expr = result.list_expr, + .body = result.body, + }, + }, region); + + return CanonicalizedExpr{ .idx = for_expr_idx, .free_vars = result.free_vars }; + }, + .malformed => { // We won't touch this since it's already a parse error. - _ = malformed; return null; }, } @@ -3181,16 +5797,100 @@ fn canonicalizeExprOrMalformed( .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ .region = self.parse_ir.tokenizedRegionToRegion(ast_expr.to_tokenized_region()), } }), - .free_vars = null, + .free_vars = DataSpan.empty(), }; }; } +/// Result of canonicalizing a for loop's components +const CanonicalizedForLoop = struct { + patt: Pattern.Idx, + list_expr: Expr.Idx, + body: Expr.Idx, + free_vars: DataSpan, +}; + +/// Canonicalize a for loop (shared between for expressions and for statements) +fn canonicalizeForLoop( + self: *Self, + ast_patt: AST.Pattern.Idx, + ast_list_expr: AST.Expr.Idx, + ast_body: AST.Expr.Idx, +) std.mem.Allocator.Error!CanonicalizedForLoop { + + // Use scratch_captures to collect free vars from both expr & body + const captures_top = self.scratch_captures.top(); + defer self.scratch_captures.clearFrom(captures_top); + + // Canonicalize the list expr + const list_expr = blk: { + const body_free_vars_start = self.scratch_free_vars.top(); + defer self.scratch_free_vars.clearFrom(body_free_vars_start); + + const czerd_expr = try self.canonicalizeExprOrMalformed(ast_list_expr); + + // Copy free vars into captures (deduplicating) + const free_vars_slice = self.scratch_free_vars.sliceFromSpan(czerd_expr.free_vars); + for (free_vars_slice) |fv| { + if (!self.scratch_captures.contains(fv)) { + try self.scratch_captures.append(fv); + } + } + + break :blk czerd_expr; + }; + + // Canonicalize the pattern + const ptrn = try self.canonicalizePatternOrMalformed(ast_patt); + + // Collect bound vars from pattern + const for_bound_vars_top = self.scratch_bound_vars.top(); + defer self.scratch_bound_vars.clearFrom(for_bound_vars_top); + try self.collectBoundVarsToScratch(ptrn); + + // Canonicalize the body + const body = blk: { + const body_free_vars_start = self.scratch_free_vars.top(); + defer self.scratch_free_vars.clearFrom(body_free_vars_start); + + const body_expr = try self.canonicalizeExprOrMalformed(ast_body); + + // Copy free vars into captures, excluding pattern-bound vars (deduplicating) + const body_free_vars_slice = self.scratch_free_vars.sliceFromSpan(body_expr.free_vars); + for (body_free_vars_slice) |fv| { + if (!self.scratch_captures.contains(fv) and !self.scratch_bound_vars.containsFrom(for_bound_vars_top, fv)) { + try self.scratch_captures.append(fv); + } + } + + break :blk body_expr; + }; + + // Copy captures to free_vars for parent + const free_vars_start = self.scratch_free_vars.top(); + const captures_slice = self.scratch_captures.sliceFromStart(captures_top); + for (captures_slice) |capture| { + try self.scratch_free_vars.append(capture); + } + const free_vars = self.scratch_free_vars.spanFrom(free_vars_start); + + return CanonicalizedForLoop{ + .patt = ptrn, + .list_expr = list_expr.idx, + .body = body.idx, + .free_vars = free_vars, + }; +} + // Canonicalize a tag expr fn canonicalizeTagExpr(self: *Self, e: AST.TagExpr, mb_args: ?AST.Expr.Span, region: base.Region) std.mem.Allocator.Error!?CanonicalizedExpr { - const tag_name = self.parse_ir.tokens.resolveIdentifier(e.token) orelse @panic("tag token is not an ident"); - const tag_name_text = self.parse_ir.env.getIdent(tag_name); - + const tag_name = self.parse_ir.tokens.resolveIdentifier(e.token) orelse { + // Parser should have validated this, but handle gracefully + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = region, + } }); + return CanonicalizedExpr{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; + }; var args_span = Expr.Span{ .span = DataSpan.empty() }; const free_vars_start = self.scratch_free_vars.top(); @@ -3213,191 +5913,444 @@ fn canonicalizeTagExpr(self: *Self, e: AST.TagExpr, mb_args: ?AST.Expr.Span, reg } // Create a single tag, open tag union for this variable - // Use a placeholder ext_var that will be handled during type checking - const ext_var = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .{ .flex_var = null }, region, TypeVar); - const tag = try self.env.types.mkTag(tag_name, @ptrCast(self.env.store.sliceExpr(args_span))); - const tag_union = try self.env.types.mkTagUnion(&[_]Tag{tag}, ext_var); - // Create the tag expression with the tag union type - const tag_expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ + const tag_expr_idx = try self.env.addExpr(CIR.Expr{ .e_tag = .{ .name = tag_name, .args = args_span, }, - }, tag_union, region); + }, region); if (e.qualifiers.span.len == 0) { - // Check if this is an unqualified nominal tag (e.g. True or False are in scope unqualified by default) - if (self.unqualified_nominal_tags.get(tag_name_text)) |nominal_type_decl| { - // Get the type variable for the nominal type declaration (e.g., Bool type) - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ - .e_nominal = .{ - .nominal_type_decl = nominal_type_decl, - .backing_expr = tag_expr_idx, - .backing_type = .tag, - }, - }, .err, region); - return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; - } - - // If this is a tag without a prefix and not in unqualified_nominal_tags, - // then it is an anonymous tag and we can just return it - return CanonicalizedExpr{ .idx = tag_expr_idx, .free_vars = null }; + // Tag without a qualifier and not a type in scope - treat as anonymous structural tag + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ .idx = tag_expr_idx, .free_vars = free_vars_span }; } else if (e.qualifiers.span.len == 1) { - // If this is a tag with a single, then is it a nominal tag and the qualifier - // is the type + // If this is a tag with a single qualifier, then it is a nominal tag and the qualifier + // is the type name. Check both local type_decls and imported types in exposed_items. - // Get the last token of the qualifiers + // Get the qualifier token const qualifier_toks = self.parse_ir.store.tokenSlice(e.qualifiers); const type_tok_idx = qualifier_toks[0]; const type_tok_ident = self.parse_ir.tokens.resolveIdentifier(type_tok_idx) orelse unreachable; const type_tok_region = self.parse_ir.tokens.resolve(type_tok_idx); - // Lookup the type ident in scope - const nominal_type_decl_stmt_idx = self.scopeLookupTypeDecl(type_tok_ident) orelse - return CanonicalizedExpr{ - .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .undeclared_type = .{ - .name = type_tok_ident, - .region = type_tok_region, - } }), - .free_vars = null, - }; - switch (self.env.store.getStatement(nominal_type_decl_stmt_idx)) { - .s_nominal_decl => { - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ - .e_nominal = .{ - .nominal_type_decl = nominal_type_decl_stmt_idx, - .backing_expr = tag_expr_idx, - .backing_type = .tag, - }, - }, .err, region); + // First, try to lookup the type as a local declaration + if (self.scopeLookupTypeDecl(type_tok_ident)) |nominal_type_decl_stmt_idx| { + switch (self.env.store.getStatement(nominal_type_decl_stmt_idx)) { + .s_nominal_decl => { + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_nominal = .{ + .nominal_type_decl = nominal_type_decl_stmt_idx, + .backing_expr = tag_expr_idx, + .backing_type = .tag, + }, + }, region); - const free_vars_slice = self.scratch_free_vars.slice(free_vars_start, self.scratch_free_vars.top()); - return CanonicalizedExpr{ - .idx = expr_idx, - .free_vars = if (free_vars_slice.len > 0) free_vars_slice else null, - }; - }, - .s_alias_decl => { - return CanonicalizedExpr{ - .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .type_alias_but_needed_nominal = .{ - .name = type_tok_ident, + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ + .idx = expr_idx, + .free_vars = free_vars_span, + }; + }, + .s_alias_decl => { + return CanonicalizedExpr{ + .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .type_alias_but_needed_nominal = .{ + .name = type_tok_ident, + .region = type_tok_region, + } }), + .free_vars = DataSpan.empty(), + }; + }, + else => { + const feature = try self.env.insertString("report an error resolved type decl in scope wasn't actually a type decl"); + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .not_implemented = .{ + .feature = feature, .region = type_tok_region, - } }), - .free_vars = null, - }; - }, - else => { - const feature = try self.env.insertString("report an error resolved type decl in scope wasn't actually a type decl"); - const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .not_implemented = .{ - .feature = feature, - .region = Region.zero(), - } }); - return CanonicalizedExpr{ - .idx = malformed_idx, - .free_vars = null, - }; - }, + } }); + return CanonicalizedExpr{ + .idx = malformed_idx, + .free_vars = DataSpan.empty(), + }; + }, + } } + + // Not found locally, check if this is an auto-imported type like Bool or Try + if (self.module_envs) |envs_map| { + if (envs_map.get(type_tok_ident)) |auto_imported_type| { + // Check if this has a statement_idx - auto-imported types from Builtin (Bool, Try, etc.) have one + // Regular module imports and primitive types (Str) don't have statement_idx + if (auto_imported_type.statement_idx) |stmt_idx| { + // This is an auto-imported type with a statement_idx - create the import and return e_nominal_external + const module_name_text = auto_imported_type.env.module_name; + const import_idx = try self.getOrCreateAutoImport(module_name_text); + + const target_node_idx = auto_imported_type.env.getExposedNodeIndexByStatementIdx(stmt_idx) orelse { + // Failed to find exposed node - return malformed expression with diagnostic + const module_ident = try self.env.insertIdent(base.Ident.for_text(module_name_text)); + const malformed = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .nested_type_not_found = .{ + .parent_name = module_ident, + .nested_name = type_tok_ident, + .region = region, + } }); + return CanonicalizedExpr{ .idx = malformed, .free_vars = DataSpan.empty() }; + }; + + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_nominal_external = .{ + .module_idx = import_idx, + .target_node_idx = target_node_idx, + .backing_expr = tag_expr_idx, + .backing_type = .tag, + }, + }, region); + + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ + .idx = expr_idx, + .free_vars = free_vars_span, + }; + } + // If no statement_idx, fall through to check exposed_items (regular module import) + } + } + + // Not found in auto-imports, check if it's an imported type from exposed_items + if (self.scopeLookupExposedItem(type_tok_ident)) |exposed_info| { + const module_name = exposed_info.module_name; + const module_name_text = self.env.getIdent(module_name); + + // Check if this module is imported in the current scope + const import_idx = self.scopeLookupImportedModule(module_name_text) orelse { + return CanonicalizedExpr{ + .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .module_not_imported = .{ + .module_name = module_name, + .region = region, + } }), + .free_vars = DataSpan.empty(), + }; + }; + + // Look up the target node index in the imported module + // Convert identifier from current module to target module's interner + const target_node_idx: u16 = blk: { + const envs_map = self.module_envs orelse { + // Module envs not available - can't resolve external type + return CanonicalizedExpr{ .idx = try self.env.pushMalformed(Expr.Idx, CIR.Diagnostic{ .type_not_exposed = .{ + .module_name = module_name, + .type_name = type_tok_ident, + .region = type_tok_region, + } }), .free_vars = DataSpan.empty() }; + }; + const auto_imported_type = envs_map.get(module_name) orelse { + // Module not in envs - can't resolve external type + return CanonicalizedExpr{ .idx = try self.env.pushMalformed(Expr.Idx, CIR.Diagnostic{ .type_not_exposed = .{ + .module_name = module_name, + .type_name = type_tok_ident, + .region = type_tok_region, + } }), .free_vars = DataSpan.empty() }; + }; + const original_name_text = self.env.getIdent(exposed_info.original_name); + const target_ident = auto_imported_type.env.common.findIdent(original_name_text) orelse { + // Type identifier doesn't exist in the target module + return CanonicalizedExpr{ .idx = try self.env.pushMalformed(Expr.Idx, CIR.Diagnostic{ .type_not_exposed = .{ + .module_name = module_name, + .type_name = type_tok_ident, + .region = type_tok_region, + } }), .free_vars = DataSpan.empty() }; + }; + break :blk auto_imported_type.env.getExposedNodeIndexById(target_ident) orelse { + // Type is not exposed by the imported module + return CanonicalizedExpr{ .idx = try self.env.pushMalformed(Expr.Idx, CIR.Diagnostic{ .type_not_exposed = .{ + .module_name = module_name, + .type_name = type_tok_ident, + .region = type_tok_region, + } }), .free_vars = DataSpan.empty() }; + }; + }; + + // Create e_nominal_external for the imported type + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_nominal_external = .{ + .module_idx = import_idx, + .target_node_idx = target_node_idx, + .backing_expr = tag_expr_idx, + .backing_type = .tag, + }, + }, region); + + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ + .idx = expr_idx, + .free_vars = free_vars_span, + }; + } + + // Not found in type_decls or exposed_items - type is undeclared + return CanonicalizedExpr{ + .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .undeclared_type = .{ + .name = type_tok_ident, + .region = type_tok_region, + } }), + .free_vars = DataSpan.empty(), + }; } else { - // If this is a tag with more than 1 qualifier, then it is an imported - // nominal type where the last qualifier is the type name, then the other - // are the module + // Multi-qualified tag (e.g., Foo.Bar.X or Foo.Bar.Baz.X) + // + // All qualifiers form the type name, with the final segment as the tag name. + // Example: Foo.Bar.Baz.X has type "Foo.Bar.Baz" and tag "X" + // + // To resolve the type, check if the first qualifier matches an imported module name. + // If it does, look up the type in that module; otherwise, look up locally. - // Get the last token of the qualifiers const qualifier_toks = self.parse_ir.store.tokenSlice(e.qualifiers); + const strip_tokens = [_]tokenize.Token.Tag{.NoSpaceDotUpperIdent}; - // Get the type from the last qualifier + // Check if the first qualifier is an imported name + const first_tok_idx = qualifier_toks[0]; + const first_tok_ident = self.parse_ir.tokens.resolveIdentifier(first_tok_idx) orelse unreachable; + const is_imported = self.scopeLookupModule(first_tok_ident) != null; + + // Build the full qualified type name from ALL qualifiers (the tag name is separate in e.token) + // For Foo.Bar.X: qualifiers=[Foo, Bar], token=X, type name="Foo.Bar" const type_tok_idx = qualifier_toks[qualifier_toks.len - 1]; const type_tok_ident = self.parse_ir.tokens.resolveIdentifier(type_tok_idx) orelse unreachable; const type_tok_region = self.parse_ir.tokens.resolve(type_tok_idx); const type_tok_text = self.env.getIdent(type_tok_ident); - // Get the fully resolved module name from all but the last qualifier - const strip_tokens = [_]tokenize.Token.Tag{.NoSpaceDotUpperIdent}; - const module_alias_text = self.parse_ir.resolveQualifiedName( - .{ .span = .{ .start = 0, .len = @intCast(qualifier_toks.len - 2) } }, - qualifier_toks[qualifier_toks.len - 2], + const full_type_name = self.parse_ir.resolveQualifiedName( + e.qualifiers, + qualifier_toks[qualifier_toks.len - 1], &strip_tokens, ); - const module_alias = try self.env.insertIdent(base.Ident.for_text(module_alias_text)); + const full_type_ident = try self.env.insertIdent(base.Ident.for_text(full_type_name)); - // Check if this is a module alias - const module_name = self.scopeLookupModule(module_alias) orelse { - // Module is not in current scope - return CanonicalizedExpr{ .idx = try self.env.pushMalformed(Expr.Idx, CIR.Diagnostic{ .module_not_imported = .{ - .module_name = module_alias, - .region = region, - } }), .free_vars = null }; - }; + if (!is_imported) { + // Local reference: look up the type locally + const nominal_type_decl_stmt_idx = self.scopeLookupTypeDecl(full_type_ident) orelse { + return CanonicalizedExpr{ + .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .undeclared_type = .{ + .name = full_type_ident, + .region = type_tok_region, + } }), + .free_vars = DataSpan.empty(), + }; + }; + + switch (self.env.store.getStatement(nominal_type_decl_stmt_idx)) { + .s_nominal_decl => { + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_nominal = .{ + .nominal_type_decl = nominal_type_decl_stmt_idx, + .backing_expr = tag_expr_idx, + .backing_type = .tag, + }, + }, region); + + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); + return CanonicalizedExpr{ + .idx = expr_idx, + .free_vars = free_vars_span, + }; + }, + .s_alias_decl => { + return CanonicalizedExpr{ + .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .type_alias_but_needed_nominal = .{ + .name = full_type_ident, + .region = type_tok_region, + } }), + .free_vars = DataSpan.empty(), + }; + }, + else => { + const feature = try self.env.insertString("report an error resolved type decl in scope wasn't actually a type decl"); + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .not_implemented = .{ + .feature = feature, + .region = type_tok_region, + } }); + return CanonicalizedExpr{ + .idx = malformed_idx, + .free_vars = DataSpan.empty(), + }; + }, + } + } + + // Import reference: look up the type in the imported file + // For Imported.Foo.Bar.X: module=Imported, type=Foo.Bar, tag=X + // qualifiers=[Imported, Foo, Bar], so type name is built from qualifiers[1..] + + const module_info = self.scopeLookupModule(first_tok_ident).?; // Already checked above + const module_name = module_info.module_name; const module_name_text = self.env.getIdent(module_name); - // Check if this module is imported in the current scope + // Check if this is imported in the current scope const import_idx = self.scopeLookupImportedModule(module_name_text) orelse { return CanonicalizedExpr{ .idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .module_not_imported = .{ .module_name = module_name, .region = region, - } }), .free_vars = null }; + } }), .free_vars = DataSpan.empty() }; }; - // Look up the target node index in the module's exposed_nodes - const target_node_idx, const type_content = blk: { + // Build the type name from all qualifiers except the first (module name) + // For Imported.Foo.Bar.X: qualifiers=[Imported, Foo, Bar], type="Foo.Bar" + const type_qualifiers_start = 1; + const type_name = if (qualifier_toks.len > type_qualifiers_start) + self.parse_ir.resolveQualifiedName( + Token.Span{ + .span = DataSpan.init( + e.qualifiers.span.start + type_qualifiers_start, + e.qualifiers.span.len - type_qualifiers_start, + ), + }, + qualifier_toks[qualifier_toks.len - 1], + &strip_tokens, + ) + else + type_tok_text; + const type_name_ident = try self.env.insertIdent(base.Ident.for_text(type_name)); + + // Look up the target node index in the imported file's exposed_nodes + const target_node_idx = blk: { const envs_map = self.module_envs orelse { - break :blk .{ 0, Content.err }; + break :blk 0; }; - const module_env = envs_map.get(module_name_text) orelse { - break :blk .{ 0, Content.err }; + const auto_imported_type = envs_map.get(module_name) orelse { + break :blk 0; }; - const target_ident = module_env.common.findIdent(type_tok_text) orelse { - // Type is not exposed by the module + const target_ident = auto_imported_type.env.common.findIdent(type_name) orelse { + // Type is not exposed by the imported file return CanonicalizedExpr{ .idx = try self.env.pushMalformed(Expr.Idx, CIR.Diagnostic{ .type_not_exposed = .{ .module_name = module_name, - .type_name = type_tok_ident, + .type_name = type_name_ident, .region = type_tok_region, - } }), .free_vars = null }; + } }), .free_vars = DataSpan.empty() }; }; - const other_module_node_id = module_env.getExposedNodeIndexById(target_ident) orelse { - // Type is not exposed by the module + const other_module_node_id = auto_imported_type.env.getExposedNodeIndexById(target_ident) orelse { + // Type is not exposed by the imported file return CanonicalizedExpr{ .idx = try self.env.pushMalformed(Expr.Idx, CIR.Diagnostic{ .type_not_exposed = .{ .module_name = module_name, - .type_name = type_tok_ident, + .type_name = type_name_ident, .region = type_tok_region, - } }), .free_vars = null }; + } }), .free_vars = DataSpan.empty() }; }; // Successfully found the target node - break :blk .{ other_module_node_id, Content{ .flex_var = null } }; + break :blk other_module_node_id; }; - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_nominal_external = .{ .module_idx = import_idx, .target_node_idx = target_node_idx, .backing_expr = tag_expr_idx, .backing_type = .tag, }, - }, type_content, region); + }, region); - const free_vars_slice = self.scratch_free_vars.slice(free_vars_start, self.scratch_free_vars.top()); + const free_vars_span = self.scratch_free_vars.spanFrom(free_vars_start); return CanonicalizedExpr{ .idx = expr_idx, - .free_vars = if (free_vars_slice.len > 0) free_vars_slice else null, + .free_vars = free_vars_span, }; } } +/// Process escape sequences in a string, returning the processed string. +/// Handles: \n, \r, \t, \\, \", \', \$, and \u(XXXX) unicode escapes. +fn processEscapeSequences(allocator: std.mem.Allocator, input: []const u8) std.mem.Allocator.Error![]const u8 { + // Quick check: if no backslashes, return the input as-is + if (std.mem.indexOfScalar(u8, input, '\\') == null) { + return input; + } + + var result = try std.ArrayList(u8).initCapacity(allocator, input.len); + var i: usize = 0; + while (i < input.len) { + if (input[i] == '\\' and i + 1 < input.len) { + const next = input[i + 1]; + switch (next) { + 'n' => { + try result.append(allocator, '\n'); + i += 2; + }, + 'r' => { + try result.append(allocator, '\r'); + i += 2; + }, + 't' => { + try result.append(allocator, '\t'); + i += 2; + }, + '\\' => { + try result.append(allocator, '\\'); + i += 2; + }, + '"' => { + try result.append(allocator, '"'); + i += 2; + }, + '\'' => { + try result.append(allocator, '\''); + i += 2; + }, + '$' => { + try result.append(allocator, '$'); + i += 2; + }, + 'u' => { + // Unicode escape: \u(XXXX) + if (i + 2 < input.len and input[i + 2] == '(') { + // Find the closing paren + if (std.mem.indexOfScalarPos(u8, input, i + 3, ')')) |close_paren| { + const hex_code = input[i + 3 .. close_paren]; + if (std.fmt.parseInt(u21, hex_code, 16)) |codepoint| { + if (std.unicode.utf8ValidCodepoint(codepoint)) { + var buf: [4]u8 = undefined; + const len = std.unicode.utf8Encode(codepoint, &buf) catch { + // Invalid, keep original + try result.append(allocator, input[i]); + i += 1; + continue; + }; + try result.appendSlice(allocator, buf[0..len]); + i = close_paren + 1; + continue; + } + } else |_| {} + } + } + // Invalid unicode escape, keep original + try result.append(allocator, input[i]); + i += 1; + }, + else => { + // Unknown escape, keep as-is + try result.append(allocator, input[i]); + i += 1; + }, + } + } else { + try result.append(allocator, input[i]); + i += 1; + } + } + return result.toOwnedSlice(allocator); +} + /// Helper function to create a string literal expression and add it to the scratch stack fn addStringLiteralToScratch(self: *Self, text: []const u8, region: AST.TokenizedRegion) std.mem.Allocator.Error!void { // intern the string in the ModuleEnv const string_idx = try self.env.insertString(text); // create a node for the string literal - const str_expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ .e_str_segment = .{ + const str_expr_idx = try self.env.addExpr(CIR.Expr{ .e_str_segment = .{ .literal = string_idx, - } }, Content{ .structure = .str }, self.parse_ir.tokenizedRegionToRegion(region)); + } }, self.parse_ir.tokenizedRegionToRegion(region)); // add the node idx to our scratch expr stack try self.env.store.addScratchExpr(str_expr_idx); @@ -3426,9 +6379,13 @@ fn extractStringSegments(self: *Self, parts: []const AST.Expr.Idx) std.mem.Alloc const part_node = self.parse_ir.store.getExpr(part); switch (part_node) { .string_part => |sp| { - // get the raw text of the string part + // get the raw text of the string part and process escape sequences const part_text = self.parse_ir.resolve(sp.token); - try self.addStringLiteralToScratch(part_text, part_node.to_tokenized_region()); + const processed_text = try processEscapeSequences(self.env.gpa, part_text); + defer if (processed_text.ptr != part_text.ptr) { + self.env.gpa.free(processed_text); + }; + try self.addStringLiteralToScratch(processed_text, part_node.to_tokenized_region()); }, else => { try self.addInterpolationToScratch(part, part_node); @@ -3450,13 +6407,17 @@ fn extractMultilineStringSegments(self: *Self, parts: []const AST.Expr.Idx) std. .string_part => |sp| { // Add newline between consecutive string parts if (last_string_part_end != null) { - try self.addStringLiteralToScratch("\\n", .{ .start = last_string_part_end.?, .end = part_node.to_tokenized_region().start }); + try self.addStringLiteralToScratch("\n", .{ .start = last_string_part_end.?, .end = part_node.to_tokenized_region().start }); } - // Get and process the raw text of the string part + // Get and process the raw text of the string part (including escape sequences) const part_text = self.parse_ir.resolve(sp.token); if (part_text.len != 0) { - try self.addStringLiteralToScratch(part_text, part_node.to_tokenized_region()); + const processed_text = try processEscapeSequences(self.env.gpa, part_text); + defer if (processed_text.ptr != part_text.ptr) { + self.env.gpa.free(processed_text); + }; + try self.addStringLiteralToScratch(processed_text, part_node.to_tokenized_region()); } last_string_part_end = part_node.to_tokenized_region().end; }, @@ -3470,48 +6431,73 @@ fn extractMultilineStringSegments(self: *Self, parts: []const AST.Expr.Idx) std. return try self.env.store.exprSpanFrom(start); } -fn canonicalizePattern( +fn canonicalizePatternOrMalformed( + self: *Self, + ast_pattern_idx: AST.Pattern.Idx, +) std.mem.Allocator.Error!Pattern.Idx { + if (try self.canonicalizePattern(ast_pattern_idx)) |idx| { + return idx; + } else { + const pattern_region = self.parse_ir.tokenizedRegionToRegion(self.parse_ir.store.getPattern(ast_pattern_idx).to_tokenized_region()); + const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .pattern_not_canonicalized = .{ + .region = pattern_region, + } }); + return malformed_idx; + } +} + +/// Converts an AST pattern into a canonical pattern, introducing identifiers into scope. +pub fn canonicalizePattern( self: *Self, ast_pattern_idx: AST.Pattern.Idx, ) std.mem.Allocator.Error!?Pattern.Idx { const trace = tracy.trace(@src()); defer trace.end(); - const gpa = self.env.gpa; switch (self.parse_ir.store.getPattern(ast_pattern_idx)) { .ident => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); if (self.parse_ir.tokens.resolveIdentifier(e.ident_tok)) |ident_idx| { // Create a Pattern node for our identifier - const pattern_idx = try self.env.addPatternAndTypeVar(Pattern{ .assign = .{ + const pattern_idx = try self.env.addPattern(Pattern{ .assign = .{ .ident = ident_idx, - } }, .{ .flex_var = null }, region); + } }, region); - // Introduce the identifier into scope mapping to this pattern node - switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident_idx, pattern_idx, false, true)) { - .success => {}, - .shadowing_warning => |shadowed_pattern_idx| { - const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); - try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ - .ident = ident_idx, - .region = region, - .original_region = original_region, - } }); - }, - .top_level_var_error => { - return try self.env.pushMalformed(Pattern.Idx, Diagnostic{ - .invalid_top_level_statement = .{ - .stmt = try self.env.insertString("var"), + // Check if a placeholder exists for this identifier in the current scope + // Placeholders are tracked in the placeholder_idents hash map + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + const placeholder_exists = self.isPlaceholder(ident_idx); + + if (placeholder_exists) { + // Replace the placeholder in the current scope + try self.updatePlaceholder(current_scope, ident_idx, pattern_idx); + } else { + // Introduce the identifier into scope mapping to this pattern node + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident_idx, pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = ident_idx, .region = region, - }, - }); - }, - .var_across_function_boundary => { - return try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .ident_already_in_scope = .{ - .ident = ident_idx, - .region = region, - } }); - }, + .original_region = original_region, + } }); + }, + .top_level_var_error => { + return try self.env.pushMalformed(Pattern.Idx, Diagnostic{ + .invalid_top_level_statement = .{ + .stmt = try self.env.insertString("var"), + .region = region, + }, + }); + }, + .var_across_function_boundary => { + return try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .ident_already_in_scope = .{ + .ident = ident_idx, + .region = region, + } }); + }, + } } return pattern_idx; @@ -3519,7 +6505,29 @@ fn canonicalizePattern( const feature = try self.env.insertString("report an error when unable to resolve identifier"); const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .not_implemented = .{ .feature = feature, - .region = Region.zero(), + .region = region, + } }); + return malformed_idx; + } + }, + .var_ident => |e| { + // Mutable variable binding in a pattern (e.g., `|var $x, y|`) + const region = self.parse_ir.tokenizedRegionToRegion(e.region); + if (self.parse_ir.tokens.resolveIdentifier(e.ident_tok)) |ident_idx| { + // Create a Pattern node for our mutable identifier + const pattern_idx = try self.env.addPattern(Pattern{ .assign = .{ + .ident = ident_idx, + } }, region); + + // Introduce the var with function boundary tracking (using scopeIntroduceVar) + _ = try self.scopeIntroduceVar(ident_idx, pattern_idx, region, true, Pattern.Idx); + + return pattern_idx; + } else { + const feature = try self.env.insertString("report an error when unable to resolve identifier"); + const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .not_implemented = .{ + .feature = feature, + .region = region, } }); return malformed_idx; } @@ -3530,14 +6538,14 @@ fn canonicalizePattern( .underscore = {}, }; - const pattern_idx = try self.env.addPatternAndTypeVar(underscore_pattern, Content{ .flex_var = null }, region); + const pattern_idx = try self.env.addPattern(underscore_pattern, region); return pattern_idx; }, .int => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); const token_text = self.parse_ir.resolve(e.number_tok); - const parsed = types.Num.parseNumLiteralWithSuffix(token_text); + const parsed = types.parseNumeralWithSuffix(token_text); // Parse the integer value const is_negated = parsed.num_text[0] == '-'; @@ -3604,87 +6612,74 @@ fn canonicalizePattern( } } else @as(i128, @bitCast(u128_val)); + // const is_negative_u1 = @as(u1, @intFromBool(is_negated)); + // const is_power_of_2 = @as(u1, @intFromBool(u128_val != 0 and (u128_val & (u128_val - 1)) == 0)); + // const is_minimum_signed = is_negative_u1 & is_power_of_2; + // const adjusted_val = u128_val - is_minimum_signed; + + // const requirements = types.Num.Int.Requirements{ + // .sign_needed = is_negated, + // .bits_needed = types.Num.Int.BitsNeeded.fromValue(adjusted_val), + // }; + // const int_requirements = types.Num.IntRequirements{ + // .sign_needed = requirements.sign_needed, + // .bits_needed = @intCast(@intFromEnum(requirements.bits_needed)), + // }; + // Calculate requirements based on the value // Special handling for minimum signed values (-128, -32768, etc.) // These are special because they have a power-of-2 magnitude that fits exactly // in their signed type. We report them as needing one less bit to make the // standard "signed types have n-1 usable bits" logic work correctly. if (parsed.suffix) |suffix| { - const type_content = blk: { + // Capture the suffix, if provided + const num_suffix: CIR.NumKind = blk: { if (std.mem.eql(u8, suffix, "u8")) { - if (u128_val > std.math.maxInt(u8)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_u8 } }; + break :blk .u8; } else if (std.mem.eql(u8, suffix, "u16")) { - if (u128_val > std.math.maxInt(u16)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_u16 } }; + break :blk .u16; } else if (std.mem.eql(u8, suffix, "u32")) { - if (u128_val > std.math.maxInt(u32)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_u32 } }; + break :blk .u32; } else if (std.mem.eql(u8, suffix, "u64")) { - if (u128_val > std.math.maxInt(u64)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_u64 } }; + break :blk .u64; } else if (std.mem.eql(u8, suffix, "u128")) { - break :blk Content{ .structure = FlatType{ .num = Num.int_u128 } }; + break :blk .u128; } else if (std.mem.eql(u8, suffix, "i8")) { - if (i128_val < std.math.minInt(i8) or i128_val > std.math.maxInt(i8)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_i8 } }; + break :blk .i8; } else if (std.mem.eql(u8, suffix, "i16")) { - if (i128_val < std.math.minInt(i16) or i128_val > std.math.maxInt(i16)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_i16 } }; + break :blk .i16; } else if (std.mem.eql(u8, suffix, "i32")) { - if (i128_val < std.math.minInt(i32) or i128_val > std.math.maxInt(i32)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_i32 } }; + break :blk .i32; } else if (std.mem.eql(u8, suffix, "i64")) { - if (i128_val < std.math.minInt(i64) or i128_val > std.math.maxInt(i64)) break :blk null; - break :blk Content{ .structure = FlatType{ .num = Num.int_i64 } }; + break :blk .i64; } else if (std.mem.eql(u8, suffix, "i128")) { - break :blk Content{ .structure = FlatType{ .num = Num.int_i128 } }; + break :blk .i128; + } else if (std.mem.eql(u8, suffix, "f32")) { + break :blk .f32; + } else if (std.mem.eql(u8, suffix, "f64")) { + break :blk .f64; + } else if (std.mem.eql(u8, suffix, "dec")) { + break :blk .dec; } else { - break :blk null; + // Invalid numeric suffix - the suffix doesn't match any known type + return try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region } }); } }; - - if (type_content) |content| { - const pattern_idx = try self.env.addPatternAndTypeVar( - .{ .int_literal = .{ .value = .{ .bytes = @bitCast(i128_val), .kind = .i128 } } }, - content, - region, - ); - return pattern_idx; - } + const pattern_idx = try self.env.addPattern( + .{ .num_literal = .{ + .value = .{ .bytes = @bitCast(i128_val), .kind = .i128 }, + .kind = num_suffix, + } }, + region, + ); + return pattern_idx; } - const is_negative_u1 = @as(u1, @intFromBool(is_negated)); - const is_power_of_2 = @as(u1, @intFromBool(u128_val != 0 and (u128_val & (u128_val - 1)) == 0)); - const is_minimum_signed = is_negative_u1 & is_power_of_2; - const adjusted_val = u128_val - is_minimum_signed; - - const requirements = types.Num.Int.Requirements{ - .sign_needed = is_negated, - .bits_needed = types.Num.Int.BitsNeeded.fromValue(adjusted_val), - }; - - const int_requirements = types.Num.IntRequirements{ - .sign_needed = requirements.sign_needed, - .bits_needed = @intCast(@intFromEnum(requirements.bits_needed)), - }; - - // For non-decimal integers (hex, binary, octal), use int_poly directly - // For decimal integers, use num_poly so they can be either Int or Frac - const is_non_decimal = int_base != DEFAULT_BASE; - - // Insert concrete type variable - const type_content = if (is_non_decimal) - Content{ .structure = .{ .num = .{ .int_unbound = int_requirements } } } - else - Content{ .structure = .{ .num = .{ .num_unbound = int_requirements } } }; - - const pattern_idx = try self.env.addPatternAndTypeVar( - Pattern{ .int_literal = .{ .value = CIR.IntValue{ - .bytes = @bitCast(i128_val), - .kind = .i128, - } } }, - type_content, + const pattern_idx = try self.env.addPattern( + Pattern{ .num_literal = .{ + .value = CIR.IntValue{ .bytes = @bitCast(i128_val), .kind = .i128 }, + .kind = .num_unbound, + } }, region, ); return pattern_idx; @@ -3694,7 +6689,7 @@ fn canonicalizePattern( // Resolve to a string slice from the source const token_text = self.parse_ir.resolve(e.number_tok); - const parsed_num = types.Num.parseNumLiteralWithSuffix(token_text); + const parsed_num = types.parseNumeralWithSuffix(token_text); if (parsed_num.suffix) |suffix| { const f64_val = std.fmt.parseFloat(f64, parsed_num.num_text) catch { @@ -3703,25 +6698,23 @@ fn canonicalizePattern( }; if (std.mem.eql(u8, suffix, "f32")) { - if (!fitsInF32(f64_val)) { + if (!CIR.fitsInF32(f64_val)) { const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region } }); return malformed_idx; } - const pattern_idx = try self.env.addPatternAndTypeVar( + const pattern_idx = try self.env.addPattern( .{ .frac_f32_literal = .{ .value = @floatCast(f64_val) } }, - .{ .structure = FlatType{ .num = .{ .frac_precision = .f32 } } }, region, ); return pattern_idx; } else if (std.mem.eql(u8, suffix, "f64")) { - const pattern_idx = try self.env.addPatternAndTypeVar( + const pattern_idx = try self.env.addPattern( .{ .frac_f64_literal = .{ .value = f64_val } }, - .{ .structure = FlatType{ .num = .{ .frac_precision = .f64 } } }, region, ); return pattern_idx; } else if (std.mem.eql(u8, suffix, "dec")) { - if (!fitsInDec(f64_val)) { + if (!CIR.fitsInDec(f64_val)) { const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region } }); return malformed_idx; } @@ -3729,9 +6722,8 @@ fn canonicalizePattern( const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region } }); return malformed_idx; }; - const pattern_idx = try self.env.addPatternAndTypeVar( - .{ .dec_literal = .{ .value = dec_val } }, - .{ .structure = FlatType{ .num = .{ .frac_precision = .dec } } }, + const pattern_idx = try self.env.addPattern( + .{ .dec_literal = .{ .value = dec_val, .has_suffix = true } }, region, ); return pattern_idx; @@ -3739,7 +6731,7 @@ fn canonicalizePattern( } const parsed = parseFracLiteral(token_text) catch |err| switch (err) { - error.InvalidNumLiteral => { + error.InvalidNumeral => { const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region, } }); @@ -3747,18 +6739,6 @@ fn canonicalizePattern( }, }; - // Parse the literal first to get requirements - const requirements = switch (parsed) { - .small => |small_info| small_info.requirements, - .dec => |dec_info| dec_info.requirements, - .f64 => |f64_info| f64_info.requirements, - }; - - const frac_requirements = types.Num.FracRequirements{ - .fits_in_f32 = requirements.fits_in_f32, - .fits_in_dec = requirements.fits_in_dec, - }; - // Check for f64 literals which are not allowed in patterns if (parsed == .f64) { const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .f64_pattern_literal = .{ @@ -3770,100 +6750,121 @@ fn canonicalizePattern( const cir_pattern = switch (parsed) { .small => |small_info| Pattern{ .small_dec_literal = .{ - .numerator = small_info.numerator, - .denominator_power_of_ten = small_info.denominator_power_of_ten, + .value = .{ + .numerator = small_info.numerator, + .denominator_power_of_ten = small_info.denominator_power_of_ten, + }, + .has_suffix = false, }, }, .dec => |dec_info| Pattern{ .dec_literal = .{ .value = dec_info.value, + .has_suffix = false, }, }, .f64 => unreachable, // Already handled above }; - const pattern_idx = try self.env.addPatternAndTypeVar(cir_pattern, Content{ - .structure = .{ .num = .{ .frac_unbound = frac_requirements } }, - }, region); + const pattern_idx = try self.env.addPattern(cir_pattern, region); return pattern_idx; }, .string => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); - // resolve to a string slice from the source - const token_text = self.parse_ir.resolve(e.string_tok); + // Get the string expression which contains the actual string parts + const str_expr = self.parse_ir.store.getExpr(e.expr); - // TODO: Handle escape sequences - // For now, just intern the raw string - const literal = try self.env.insertString(token_text); + switch (str_expr) { + .string => |se| { + // Get the parts of the string expression + const parts = self.parse_ir.store.exprSlice(se.parts); - const str_pattern = Pattern{ - .str_literal = .{ - .literal = literal, + // For simple string literals, there should be exactly one string_part + if (parts.len == 1) { + const part = self.parse_ir.store.getExpr(parts[0]); + switch (part) { + .string_part => |sp| { + // Get the actual string content from the string_part token + const part_text = self.parse_ir.resolve(sp.token); + + // Process escape sequences + const processed_text = try processEscapeSequences(self.env.gpa, part_text); + defer if (processed_text.ptr != part_text.ptr) { + self.env.gpa.free(processed_text); + }; + + const literal = try self.env.insertString(processed_text); + + const str_pattern = Pattern{ + .str_literal = .{ + .literal = literal, + }, + }; + const pattern_idx = try self.env.addPattern(str_pattern, region); + + return pattern_idx; + }, + else => {}, + } + } + + // For string patterns with interpolation or multiple parts, + // we need more complex handling (not yet supported) + const malformed = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ + .not_implemented = .{ + .feature = try self.env.insertString("string patterns with interpolation"), + .region = region, + }, + }); + return malformed; }, - }; - const pattern_idx = try self.env.addPatternAndTypeVar(str_pattern, Content{ .structure = .str }, region); - - return pattern_idx; + else => { + // Unexpected expression type in string pattern + const malformed = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ + .pattern_arg_invalid = .{ + .region = region, + }, + }); + return malformed; + }, + } }, .single_quote => |e| { return try self.canonicalizeSingleQuote(e.region, e.token, Pattern.Idx); }, .tag => |e| { const tag_name = self.parse_ir.tokens.resolveIdentifier(e.tag_tok) orelse return null; - const tag_name_text = self.parse_ir.env.getIdent(tag_name); const region = self.parse_ir.tokenizedRegionToRegion(e.region); // Canonicalized the tags args - const patterns_start = self.env.store.scratch_patterns.top(); + const patterns_start = self.env.store.scratch.?.patterns.top(); for (self.parse_ir.store.patternSlice(e.args)) |sub_ast_pattern_idx| { if (try self.canonicalizePattern(sub_ast_pattern_idx)) |idx| { - try self.env.store.scratch_patterns.append(gpa, idx); + try self.env.store.scratch.?.patterns.append(idx); } else { const arg = self.parse_ir.store.getPattern(sub_ast_pattern_idx); const arg_region = self.parse_ir.tokenizedRegionToRegion(arg.to_tokenized_region()); const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .pattern_arg_invalid = .{ .region = arg_region, } }); - try self.env.store.scratch_patterns.append(gpa, malformed_idx); + try self.env.store.scratch.?.patterns.append(malformed_idx); } } const args = try self.env.store.patternSpanFrom(patterns_start); - // Create the pattern type var first - const arg_vars: []TypeVar = @ptrCast(self.env.store.slicePatterns(args)); - // We need to create a temporary pattern idx to get the type var - const ext_var = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .{ .flex_var = null }, region, TypeVar); - const tag = try self.env.types.mkTag(tag_name, arg_vars); - const tag_union_type = try self.env.types.mkTagUnion(&[_]Tag{tag}, ext_var); - // Create the pattern node with type var - const tag_pattern_idx = try self.env.addPatternAndTypeVar(Pattern{ + const tag_pattern_idx = try self.env.addPattern(Pattern{ .applied_tag = .{ .name = tag_name, .args = args, }, - }, tag_union_type, region); + }, region); if (e.qualifiers.span.len == 0) { - // Check if this is an unqualified nominal tag (e.g. True or False are in scope unqualified by default) - if (self.unqualified_nominal_tags.get(tag_name_text)) |nominal_type_decl| { - // Get the type variable for the nominal type declaration (e.g., Bool type) - const nominal_type_var = ModuleEnv.castIdx(Statement.Idx, TypeVar, nominal_type_decl); - const nominal_pattern_idx = try self.env.addPatternAndTypeVarRedirect(CIR.Pattern{ - .nominal = .{ - .nominal_type_decl = nominal_type_decl, - .backing_pattern = tag_pattern_idx, - .backing_type = .tag, - }, - }, nominal_type_var, region); - return nominal_pattern_idx; - } - - // If this is a tag without a prefix and not in unqualified_nominal_tags, - // then it is an anonymous tag and we can just return it + // Tag without a qualifier is an anonymous structural tag return tag_pattern_idx; } else if (e.qualifiers.span.len == 1) { // If this is a tag with a single, then is it a nominal tag and the qualifier is the type @@ -3883,14 +6884,13 @@ fn canonicalizePattern( switch (self.env.store.getStatement(nominal_type_decl_stmt_idx)) { .s_nominal_decl => { - const nominal_type_var = ModuleEnv.castIdx(Statement.Idx, TypeVar, nominal_type_decl_stmt_idx); - const pattern_idx = try self.env.addPatternAndTypeVarRedirect(CIR.Pattern{ + const pattern_idx = try self.env.addPattern(CIR.Pattern{ .nominal = .{ .nominal_type_decl = nominal_type_decl_stmt_idx, .backing_pattern = tag_pattern_idx, .backing_type = .tag, }, - }, nominal_type_var, region); + }, region); return pattern_idx; }, @@ -3904,7 +6904,7 @@ fn canonicalizePattern( const feature = try self.env.insertString("report an error resolved type decl in scope wasn't actually a type decl"); return try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .not_implemented = .{ .feature = feature, - .region = Region.zero(), + .region = type_tok_region, } }); }, } @@ -3932,13 +6932,14 @@ fn canonicalizePattern( const module_alias = try self.env.insertIdent(base.Ident.for_text(module_alias_text)); // Check if this is a module alias - const module_name = self.scopeLookupModule(module_alias) orelse { + const module_info = self.scopeLookupModule(module_alias) orelse { // Module is not in current scope return try self.env.pushMalformed(Pattern.Idx, CIR.Diagnostic{ .module_not_imported = .{ .module_name = module_alias, .region = region, } }); }; + const module_name = module_info.module_name; const module_name_text = self.env.getIdent(module_name); // Check if this module is imported in the current scope @@ -3950,16 +6951,16 @@ fn canonicalizePattern( }; // Look up the target node index in the module's exposed_nodes - const target_node_idx, const type_content = blk: { + const target_node_idx, _ = blk: { const envs_map = self.module_envs orelse { break :blk .{ 0, Content.err }; }; - const module_env = envs_map.get(module_name_text) orelse { + const auto_imported_type = envs_map.get(module_name) orelse { break :blk .{ 0, Content.err }; }; - const target_ident = module_env.common.findIdent(type_tok_text) orelse { + const target_ident = auto_imported_type.env.common.findIdent(type_tok_text) orelse { // Type is not exposed by the module return try self.env.pushMalformed(Pattern.Idx, CIR.Diagnostic{ .type_not_exposed = .{ .module_name = module_name, @@ -3968,7 +6969,7 @@ fn canonicalizePattern( } }); }; - const other_module_node_id = module_env.getExposedNodeIndexById(target_ident) orelse { + const other_module_node_id = auto_imported_type.env.getExposedNodeIndexById(target_ident) orelse { // Type is not exposed by the module return try self.env.pushMalformed(Pattern.Idx, CIR.Diagnostic{ .type_not_exposed = .{ .module_name = module_name, @@ -3978,17 +6979,17 @@ fn canonicalizePattern( }; // Successfully found the target node - break :blk .{ other_module_node_id, Content{ .flex_var = null } }; + break :blk .{ other_module_node_id, Content{ .flex = types.Flex.init() } }; }; - const nominal_pattern_idx = try self.env.addPatternAndTypeVar(CIR.Pattern{ + const nominal_pattern_idx = try self.env.addPattern(CIR.Pattern{ .nominal_external = .{ .module_idx = import_idx, .target_node_idx = target_node_idx, .backing_pattern = tag_pattern_idx, .backing_type = .tag, }, - }, type_content, region); + }, region); return nominal_pattern_idx; } @@ -4009,12 +7010,14 @@ fn canonicalizePattern( // For simple destructuring like `{ name, age }`, both label and ident are the same if (field.value) |sub_pattern_idx| { // Handle patterns like `{ name: x }` or `{ address: { city } }` where there's a sub-pattern - const canonicalized_sub_pattern = try self.canonicalizePattern(sub_pattern_idx) orelse { - // If sub-pattern canonicalization fails, return malformed pattern - const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .pattern_not_canonicalized = .{ - .region = field_region, - } }); - return malformed_idx; + const canonicalized_sub_pattern = blk: { + break :blk try self.canonicalizePattern(sub_pattern_idx) orelse { + // If sub-pattern canonicalization fails, return malformed pattern + const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .pattern_not_canonicalized = .{ + .region = field_region, + } }); + break :blk malformed_idx; + }; }; // Create the RecordDestruct with sub-pattern @@ -4024,12 +7027,12 @@ fn canonicalizePattern( .kind = .{ .SubPattern = canonicalized_sub_pattern }, }; - const destruct_idx = try self.env.addRecordDestructAndTypeVar(record_destruct, .{ .flex_var = null }, field_region); + const destruct_idx = try self.env.addRecordDestruct(record_destruct, field_region); try self.env.store.addScratchRecordDestruct(destruct_idx); } else { // Simple case: Create the RecordDestruct for this field const assign_pattern = Pattern{ .assign = .{ .ident = field_name_ident } }; - const assign_pattern_idx = try self.env.addPatternAndTypeVar(assign_pattern, .{ .flex_var = null }, field_region); + const assign_pattern_idx = try self.env.addPattern(assign_pattern, field_region); const record_destruct = CIR.Pattern.RecordDestruct{ .label = field_name_ident, @@ -4037,7 +7040,7 @@ fn canonicalizePattern( .kind = .{ .Required = assign_pattern_idx }, }; - const destruct_idx = try self.env.addRecordDestructAndTypeVar(record_destruct, .{ .flex_var = null }, field_region); + const destruct_idx = try self.env.addRecordDestruct(record_destruct, field_region); try self.env.store.addScratchRecordDestruct(destruct_idx); // Introduce the identifier into scope @@ -4082,19 +7085,12 @@ fn canonicalizePattern( // Create span of the new scratch record destructs const destructs_span = try self.env.store.recordDestructSpanFrom(scratch_top); - // Create type variables for the record - // TODO: Remove `var`s from pattern node? - const whole_var = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .{ .flex_var = null }, region, TypeVar); - const ext_var = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .{ .flex_var = null }, region, TypeVar); - // Create the record destructure pattern - const pattern_idx = try self.env.addPatternAndTypeVar(Pattern{ + const pattern_idx = try self.env.addPattern(Pattern{ .record_destructure = .{ - .whole_var = whole_var, - .ext_var = ext_var, .destructs = destructs_span, }, - }, .{ .flex_var = null }, region); + }, region); return pattern_idx; }, @@ -4117,19 +7113,11 @@ fn canonicalizePattern( // Create span of the new scratch patterns const patterns_span = try self.env.store.patternSpanFrom(scratch_top); - // Since pattern idx map 1-to-1 to variables, we can get cast the - // slice of and cast them to vars - const elems_var_range = try self.env.types.appendVars( - @ptrCast(@alignCast(self.env.store.slicePatterns(patterns_span))), - ); - - const pattern_idx = try self.env.addPatternAndTypeVar(Pattern{ + const pattern_idx = try self.env.addPattern(Pattern{ .tuple = .{ .patterns = patterns_span, }, - }, Content{ .structure = FlatType{ - .tuple = types.Tuple{ .elems = elems_var_range }, - } }, region); + }, region); return pattern_idx; }, @@ -4167,11 +7155,9 @@ fn canonicalizePattern( // Create an assign pattern for the rest variable // Use the region of just the identifier token, not the full rest pattern const name_region = self.parse_ir.tokenizedRegionToRegion(.{ .start = name_tok, .end = name_tok }); - // Note: The rest variable's type will be set later when we know elem_var - // For now, just give it a flex var - const assign_idx = try self.env.addPatternAndTypeVar(Pattern{ .assign = .{ + const assign_idx = try self.env.addPattern(Pattern{ .assign = .{ .ident = ident_idx, - } }, Content{ .flex_var = null }, name_region); + } }, name_region); // Introduce the identifier into scope switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident_idx, assign_idx, false, true)) { @@ -4207,19 +7193,19 @@ fn canonicalizePattern( // Store rest information // The rest_index should be the number of patterns canonicalized so far - const patterns_so_far = self.env.store.scratch_patterns.top() - scratch_top; + const patterns_so_far = self.env.store.scratch.?.patterns.top() - scratch_top; rest_index = @intCast(patterns_so_far); rest_pattern = current_rest_pattern; } else { // Regular pattern - canonicalize it and add to scratch patterns if (try self.canonicalizePattern(pattern_idx)) |canonicalized| { - try self.env.store.scratch_patterns.append(gpa, canonicalized); + try self.env.store.scratch.?.patterns.append(canonicalized); } else { const pattern_region = self.parse_ir.tokenizedRegionToRegion(ast_pattern.to_tokenized_region()); const malformed_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .pattern_not_canonicalized = .{ .region = pattern_region, } }); - try self.env.store.scratch_patterns.append(gpa, malformed_idx); + try self.env.store.scratch.?.patterns.append(malformed_idx); } } } @@ -4229,50 +7215,25 @@ fn canonicalizePattern( // Handle empty list patterns specially if (patterns_span.span.len == 0 and rest_index == null) { - // Empty list pattern - create a simple pattern without elem_var - const pattern_idx = try self.env.addPatternAndTypeVar(Pattern{ + // Empty list pattern + const pattern_idx = try self.env.addPattern(Pattern{ .list = .{ - .list_var = @enumFromInt(0), // Will be set by addPatternAndTypeVar - .elem_var = @enumFromInt(0), // Not used for empty lists .patterns = patterns_span, .rest_info = null, }, - }, Content{ .structure = .list_unbound }, region); + }, region); return pattern_idx; } - // For non-empty list patterns, use the first pattern's type variable as elem_var - const elem_var: TypeVar = if (patterns_span.span.len > 0) blk: { - const first_pattern_idx = self.env.store.slicePatterns(patterns_span)[0]; - break :blk @enumFromInt(@intFromEnum(first_pattern_idx)); - } else blk: { - // Must be a rest-only pattern like [..] or [.. as rest] - // Create a placeholder pattern for the element type - const placeholder_idx = try self.env.addPatternAndTypeVar(Pattern{ - .underscore = {}, - }, Content{ .flex_var = null }, region); - break :blk @enumFromInt(@intFromEnum(placeholder_idx)); - }; - - // Update rest pattern's type if it exists - if (rest_pattern) |rest_pat| { - // Update the rest pattern's type to be a list of elem_var - const rest_list_type = Content{ .structure = .{ .list = elem_var } }; - _ = try self.env.types.setVarContent(@enumFromInt(@intFromEnum(rest_pat)), rest_list_type); - } - // Create the list pattern with rest info // Set type variable for the pattern - this should be the list type - const list_type = Content{ .structure = .{ .list = elem_var } }; - const pattern_idx = try self.env.addPatternAndTypeVar(Pattern{ + const pattern_idx = try self.env.addPattern(Pattern{ .list = .{ - .list_var = @enumFromInt(0), // Will be set by addPatternAndTypeVar - .elem_var = elem_var, .patterns = patterns_span, .rest_info = if (rest_index) |idx| .{ .index = idx, .pattern = rest_pattern } else null, }, - }, list_type, region); + }, region); return pattern_idx; }, @@ -4285,13 +7246,14 @@ fn canonicalizePattern( } }); return pattern_idx; }, - .alternatives => |_| { + .alternatives => |alt| { // Alternatives patterns should only appear in match expressions and are handled there // If we encounter one here, it's likely a parser error or misplaced pattern + const region = self.parse_ir.tokenizedRegionToRegion(alt.region); const feature = try self.env.insertString("alternatives pattern outside match expression"); const pattern_idx = try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .not_implemented = .{ .feature = feature, - .region = Region.zero(), + .region = region, } }); return pattern_idx; }, @@ -4318,7 +7280,7 @@ fn canonicalizePattern( }, }; - const pattern_idx = try self.env.addPatternAndTypeVar(as_pattern, .{ .flex_var = null }, region); + const pattern_idx = try self.env.addPattern(as_pattern, region); // Introduce the identifier into scope switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident_idx, pattern_idx, false, true)) { @@ -4357,9 +7319,8 @@ fn canonicalizePattern( return pattern_idx; } }, - .malformed => |malformed| { + .malformed => { // We won't touch this since it's already a parse error. - _ = malformed; return null; }, } @@ -4367,7 +7328,7 @@ fn canonicalizePattern( /// Enter a function boundary by pushing its region onto the stack fn enterFunction(self: *Self, region: Region) std.mem.Allocator.Error!void { - try self.function_regions.append(self.env.gpa, region); + try self.function_regions.append(region); } /// Exit a function boundary by popping from the stack @@ -4408,39 +7369,20 @@ fn isVarReassignmentAcrossFunctionBoundary(self: *const Self, pattern_idx: Patte return false; } -// Check if the given f64 fits in f32 range (ignoring precision loss) -fn fitsInF32(f64_val: f64) bool { - // Check if it's within the range that f32 can represent. - // This includes normal, subnormal, and zero values. - // (This is a magnitude check, so take the abs value to check - // positive and negative at the same time.) - const abs_val = @abs(f64_val); - return abs_val == 0.0 or (abs_val >= std.math.floatTrueMin(f32) and abs_val <= std.math.floatMax(f32)); -} - -// Check if a float value can be represented accurately in RocDec -fn fitsInDec(value: f64) bool { - // RocDec uses i128 with 18 decimal places - const max_dec_value = 170141183460469231731.0; - const min_dec_value = -170141183460469231731.0; - - return value >= min_dec_value and value <= max_dec_value; -} - // Result type for parsing fractional literals into small, Dec, or f64 const FracLiteralResult = union(enum) { small: struct { numerator: i16, denominator_power_of_ten: u8, - requirements: types.Num.Frac.Requirements, + requirements: types.Frac.Requirements, }, dec: struct { value: RocDec, - requirements: types.Num.Frac.Requirements, + requirements: types.Frac.Requirements, }, f64: struct { value: f64, - requirements: types.Num.Frac.Requirements, + requirements: types.Frac.Requirements, }, }; @@ -4501,7 +7443,7 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { // First, always parse as f64 to get the numeric value const f64_val = std.fmt.parseFloat(f64, token_text) catch { // If it can't be parsed as F64, it's too big to fit in any of Roc's Frac types. - return error.InvalidNumLiteral; + return error.InvalidNumeral; }; // Check if it has scientific notation @@ -4530,8 +7472,8 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { .small = .{ .numerator = small.numerator, .denominator_power_of_ten = small.denominator_power_of_ten, - .requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = fitsInF32(small_f64_val), + .requirements = types.Frac.Requirements{ + .fits_in_f32 = CIR.fitsInF32(small_f64_val), .fits_in_dec = true, }, }, @@ -4547,8 +7489,8 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { .small = .{ .numerator = @as(i16, @intFromFloat(rounded)), .denominator_power_of_ten = 0, - .requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = fitsInF32(f64_val), + .requirements = types.Frac.Requirements{ + .fits_in_f32 = CIR.fitsInF32(f64_val), .fits_in_dec = true, }, }, @@ -4558,7 +7500,7 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { // Check if the value can fit in RocDec (whether or not it uses scientific notation) // RocDec uses i128 with 18 decimal places // We need to check if the value is within RocDec's range - if (fitsInDec(f64_val)) { + if (CIR.fitsInDec(f64_val)) { // Convert f64 to RocDec by multiplying by 10^18 const dec_scale = std.math.pow(f64, 10, 18); const scaled_val = f64_val * dec_scale; @@ -4579,8 +7521,8 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { return FracLiteralResult{ .f64 = .{ .value = f64_val, - .requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = fitsInF32(f64_val), + .requirements = types.Frac.Requirements{ + .fits_in_f32 = CIR.fitsInF32(f64_val), .fits_in_dec = false, }, }, @@ -4597,8 +7539,8 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { return FracLiteralResult{ .f64 = .{ .value = f64_val, - .requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = fitsInF32(f64_val), + .requirements = types.Frac.Requirements{ + .fits_in_f32 = CIR.fitsInF32(f64_val), .fits_in_dec = false, }, }, @@ -4608,8 +7550,8 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { return FracLiteralResult{ .dec = .{ .value = RocDec{ .num = dec_num }, - .requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = fitsInF32(f64_val), + .requirements = types.Frac.Requirements{ + .fits_in_f32 = CIR.fitsInF32(f64_val), .fits_in_dec = true, }, }, @@ -4621,8 +7563,8 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { return FracLiteralResult{ .f64 = .{ .value = f64_val, - .requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = fitsInF32(f64_val), + .requirements = types.Frac.Requirements{ + .fits_in_f32 = CIR.fitsInF32(f64_val), .fits_in_dec = false, }, }, @@ -4719,16 +7661,16 @@ fn scopeIntroduceVar( } } -fn collectTypeVarProblems(ident: Ident.Idx, is_single_use: bool, ast_anno: AST.TypeAnno.Idx, gpa: std.mem.Allocator, scratch: *base.Scratch(TypeVarProblem)) void { - // Warn for type variables with trailing underscores +fn collectTypeVarProblems(ident: Ident.Idx, is_single_use: bool, ast_anno: AST.TypeAnno.Idx, scratch: *base.Scratch(TypeVarProblem)) std.mem.Allocator.Error!void { + // Warn for type variables starting with dollar sign (reusable markers) if (ident.attributes.reassignable) { - scratch.append(gpa, .{ .ident = ident, .problem = .type_var_ending_in_underscore, .ast_anno = ast_anno }); + try scratch.append(.{ .ident = ident, .problem = .type_var_starting_with_dollar, .ast_anno = ast_anno }); } // Should start with underscore but doesn't, or should not start with underscore but does. if (is_single_use != ident.attributes.ignored) { const problem_type: TypeVarProblemKind = if (is_single_use) .unused_type_var else .type_var_marked_unused; - scratch.append(gpa, .{ .ident = ident, .problem = problem_type, .ast_anno = ast_anno }); + try scratch.append(.{ .ident = ident, .problem = problem_type, .ast_anno = ast_anno }); } } @@ -4738,11 +7680,11 @@ fn reportTypeVarProblems(self: *Self, problems: []const TypeVarProblem) std.mem. const name_text = self.env.getIdent(problem.ident); switch (problem.problem) { - .type_var_ending_in_underscore => { - const suggested_name_text = name_text[0 .. name_text.len - 1]; // Remove the trailing underscore + .type_var_starting_with_dollar => { + const suggested_name_text = name_text[1..]; // Remove the leading dollar sign const suggested_ident = self.env.insertIdent(base.Ident.for_text(suggested_name_text), Region.zero()); - self.env.pushDiagnostic(Diagnostic{ .type_var_ending_in_underscore = .{ + self.env.pushDiagnostic(Diagnostic{ .type_var_starting_with_dollar = .{ .name = problem.ident, .suggested_name = suggested_ident, .region = region, @@ -4799,8 +7741,8 @@ fn processCollectedTypeVars(self: *Self) std.mem.Allocator.Error!void { // Collect problems for this type variable const is_single_use = !found_another; - // Use a dummy AST annotation index since we don't have the context - collectTypeVarProblems(first_ident, is_single_use, @enumFromInt(0), self.env.gpa, &self.scratch_type_var_problems); + // Use undefined AST annotation index since we don't have the context here + try collectTypeVarProblems(first_ident, is_single_use, undefined, &self.scratch_type_var_problems); } // Report any problems we found @@ -4810,11 +7752,11 @@ fn processCollectedTypeVars(self: *Self) std.mem.Allocator.Error!void { const name_text = self.env.getIdent(problem.ident); switch (problem.problem) { - .type_var_ending_in_underscore => { - const suggested_name_text = name_text[0 .. name_text.len - 1]; // Remove the trailing underscore + .type_var_starting_with_dollar => { + const suggested_name_text = name_text[1..]; // Remove the leading dollar sign const suggested_ident = self.env.insertIdent(base.Ident.for_text(suggested_name_text), Region.zero()); - self.env.pushDiagnostic(Diagnostic{ .type_var_ending_in_underscore = .{ + self.env.pushDiagnostic(Diagnostic{ .type_var_starting_with_dollar = .{ .name = problem.ident, .suggested_name = suggested_ident, .region = Region.zero(), @@ -4841,7 +7783,7 @@ fn processCollectedTypeVars(self: *Self) std.mem.Allocator.Error!void { } } -// ===== Canonicalize Type Annotations ===== +// Canonicalize Type Annotations // Some type annotations, like function type annotations, can introduce variables. // Others, however, like alias or nominal tag annotations, cannot. @@ -4882,15 +7824,14 @@ fn canonicalizeTypeAnnoHelp(self: *Self, anno_idx: AST.TypeAnno.Idx, type_anno_c } }); }; // Check if this type variable is in scope - const scope = self.currentScope(); - switch (scope.lookupTypeVar(name_ident)) { + switch (self.scopeLookupTypeVar(name_ident)) { .found => |found_anno_idx| { // Track this type variable for underscore validation - try self.scratch_type_var_validation.append(self.env.gpa, name_ident); + try self.scratch_type_var_validation.append(name_ident); - return try self.env.addTypeAnnoAndTypeVarRedirect(.{ .ty_var = .{ - .name = name_ident, - } }, ModuleEnv.varFrom(found_anno_idx), region); + return try self.env.addTypeAnno(.{ .rigid_var_lookup = .{ + .ref = found_anno_idx, + } }, region); }, .not_found => { switch (type_anno_ctx.type) { @@ -4898,12 +7839,11 @@ fn canonicalizeTypeAnnoHelp(self: *Self, anno_idx: AST.TypeAnno.Idx, type_anno_c // into the scope .inline_anno => { // Track this type variable for underscore validation - try self.scratch_type_var_validation.append(self.env.gpa, name_ident); + try self.scratch_type_var_validation.append(name_ident); - const content = types.Content{ .rigid_var = name_ident }; - const new_anno_idx = try self.env.addTypeAnnoAndTypeVar(.{ .ty_var = .{ + const new_anno_idx = try self.env.addTypeAnno(.{ .rigid_var = .{ .name = name_ident, - } }, content, region); + } }, region); // Add to scope _ = try self.scopeIntroduceTypeVar(name_ident, new_anno_idx); @@ -4941,15 +7881,14 @@ fn canonicalizeTypeAnnoHelp(self: *Self, anno_idx: AST.TypeAnno.Idx, type_anno_c }; // Check if this type variable is in scope - const scope = self.currentScope(); - switch (scope.lookupTypeVar(name_ident)) { + switch (self.scopeLookupTypeVar(name_ident)) { .found => |found_anno_idx| { // Track this type variable for underscore validation - try self.scratch_type_var_validation.append(self.env.gpa, name_ident); + try self.scratch_type_var_validation.append(name_ident); - return try self.env.addTypeAnnoAndTypeVarRedirect(.{ .ty_var = .{ - .name = name_ident, - } }, ModuleEnv.varFrom(found_anno_idx), region); + return try self.env.addTypeAnno(.{ .rigid_var_lookup = .{ + .ref = found_anno_idx, + } }, region); }, .not_found => { switch (type_anno_ctx.type) { @@ -4957,12 +7896,11 @@ fn canonicalizeTypeAnnoHelp(self: *Self, anno_idx: AST.TypeAnno.Idx, type_anno_c // into the scope .inline_anno => { // Track this type variable for underscore validation - try self.scratch_type_var_validation.append(self.env.gpa, name_ident); + try self.scratch_type_var_validation.append(name_ident); - const content = types.Content{ .rigid_var = name_ident }; - const new_anno_idx = try self.env.addTypeAnnoAndTypeVar(.{ .ty_var = .{ + const new_anno_idx = try self.env.addTypeAnno(.{ .rigid_var = .{ .name = name_ident, - } }, content, region); + } }, region); // Add to scope _ = try self.scopeIntroduceTypeVar(name_ident, new_anno_idx); @@ -4981,7 +7919,7 @@ fn canonicalizeTypeAnnoHelp(self: *Self, anno_idx: AST.TypeAnno.Idx, type_anno_c } }, .ty => |ty| { - return (try self.canonicalizeTypeAnnoBasicType(ty)).anno_idx; + return try self.canonicalizeTypeAnnoBasicType(ty); }, .underscore => |underscore| { type_anno_ctx.found_underscore = true; @@ -4996,16 +7934,7 @@ fn canonicalizeTypeAnnoHelp(self: *Self, anno_idx: AST.TypeAnno.Idx, type_anno_c } }); } - // Create type variable with error content if underscore in type declaration - const content = blk: { - if (type_anno_ctx.isTypeDeclAndHasUnderscore()) { - break :blk types.Content{ .err = {} }; - } else { - break :blk types.Content{ .flex_var = null }; - } - }; - - return try self.env.addTypeAnnoAndTypeVar(.{ .underscore = {} }, content, region); + return try self.env.addTypeAnno(.{ .underscore = {} }, region); }, .tuple => |tuple| { return try self.canonicalizeTypeAnnoTuple(tuple, type_anno_ctx); @@ -5025,13 +7954,13 @@ fn canonicalizeTypeAnnoHelp(self: *Self, anno_idx: AST.TypeAnno.Idx, type_anno_c // Create type variable with error content if underscore in type declaration if (type_anno_ctx.isTypeDeclAndHasUnderscore()) { - return try self.env.addTypeAnnoAndTypeVar(.{ .parens = .{ + return try self.env.addTypeAnno(.{ .parens = .{ .anno = inner_anno, - } }, .err, region); + } }, region); } else { - return try self.env.addTypeAnnoAndTypeVarRedirect(.{ .parens = .{ + return try self.env.addTypeAnno(.{ .parens = .{ .anno = inner_anno, - } }, ModuleEnv.varFrom(inner_anno), region); + } }, region); } }, .malformed => |malformed| { @@ -5046,16 +7975,11 @@ fn canonicalizeTypeAnnoHelp(self: *Self, anno_idx: AST.TypeAnno.Idx, type_anno_c try self.processCollectedTypeVars(); } -const CanonicalizedTypeAnnoBasicType = struct { - anno_idx: TypeAnno.Idx, - mb_local_decl_idx: ?Statement.Idx, -}; - /// Handle basic type lookup (Bool, Str, Num, etc.) fn canonicalizeTypeAnnoBasicType( self: *Self, ty: @TypeOf(@as(AST.TypeAnno, undefined).ty), -) std.mem.Allocator.Error!CanonicalizedTypeAnnoBasicType { +) std.mem.Allocator.Error!TypeAnno.Idx { const trace = tracy.trace(@src()); defer trace.end(); @@ -5069,118 +7993,262 @@ fn canonicalizeTypeAnnoBasicType( const type_name_region = self.parse_ir.tokens.resolve(ty.token); if (qualifier_toks.len == 0) { - // Unqualified type + // First, check if the type is a builtin type + // There are always automatically in-scope + if (TypeAnno.Builtin.fromBytes(self.env.getIdentText(type_name_ident))) |builtin_type| { + return try self.env.addTypeAnno(CIR.TypeAnno{ .lookup = .{ + .name = type_name_ident, + .base = .{ .builtin = builtin_type }, + } }, region); + } else { + // If it's not a builtin, look up in scope using unified type bindings + if (self.scopeLookupTypeBinding(type_name_ident)) |binding_location| { + const binding = binding_location.binding.*; + return switch (binding) { + .local_nominal => |stmt| try self.env.addTypeAnno(CIR.TypeAnno{ .lookup = .{ + .name = type_name_ident, + .base = .{ .local = .{ .decl_idx = stmt } }, + } }, region), + .local_alias => |stmt| try self.env.addTypeAnno(CIR.TypeAnno{ .lookup = .{ + .name = type_name_ident, + .base = .{ .local = .{ .decl_idx = stmt } }, + } }, region), + .associated_nominal => |stmt| try self.env.addTypeAnno(CIR.TypeAnno{ .lookup = .{ + .name = type_name_ident, + .base = .{ .local = .{ .decl_idx = stmt } }, + } }, region), + .external_nominal => |external| blk: { + const import_idx = external.import_idx orelse { + break :blk try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .module_not_imported = .{ + .module_name = external.module_ident, + .region = type_name_region, + } }); + }; - // TODO: Check for List, Box, and Str here (since they are primitives) + const target_node_idx = external.target_node_idx orelse { + // Check if the module was not found + if (external.module_not_found) { + break :blk try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .type_from_missing_module = .{ + .module_name = external.module_ident, + .type_name = type_name_ident, + .region = type_name_region, + } }); + } else { + break :blk try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .type_not_exposed = .{ + .module_name = external.module_ident, + .type_name = type_name_ident, + .region = type_name_region, + } }); + } + }; - const type_decl_idx = self.scopeLookupTypeDecl(type_name_ident) orelse { - // Type not found in scope - issue diagnostic - try self.env.pushDiagnostic(Diagnostic{ .undeclared_type = .{ + break :blk try self.env.addTypeAnno(CIR.TypeAnno{ .lookup = .{ + .name = type_name_ident, + .base = .{ .external = .{ + .module_idx = import_idx, + .target_node_idx = target_node_idx, + } }, + } }, region); + }, + }; + } + + // Check if this is an auto-imported type from module_envs + if (self.module_envs) |envs_map| { + if (envs_map.get(type_name_ident)) |auto_imported_type| { + // This is an auto-imported type like Bool or Try + // We need to create an import for it and return the type annotation + const module_name_text = auto_imported_type.env.module_name; + const import_idx = try self.getOrCreateAutoImport(module_name_text); + + // Get the target node index using the pre-computed statement_idx + const stmt_idx = auto_imported_type.statement_idx orelse { + // Str doesn't have a statement_idx because it's a primitive builtin type + // It should be detected as a builtin type before reaching this code path + // Return malformed type annotation with diagnostic + const module_ident = try self.env.insertIdent(base.Ident.for_text(module_name_text)); + return try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .nested_type_not_found = .{ + .parent_name = module_ident, + .nested_name = type_name_ident, + .region = region, + } }); + }; + const target_node_idx = auto_imported_type.env.getExposedNodeIndexByStatementIdx(stmt_idx) orelse { + // Failed to find exposed node - return malformed type annotation with diagnostic + const module_ident = try self.env.insertIdent(base.Ident.for_text(module_name_text)); + return try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .nested_type_not_found = .{ + .parent_name = module_ident, + .nested_name = type_name_ident, + .region = region, + } }); + }; + + return try self.env.addTypeAnno(CIR.TypeAnno{ .lookup = .{ + .name = type_name_ident, + .base = .{ .external = .{ + .module_idx = import_idx, + .target_node_idx = target_node_idx, + } }, + } }, region); + } + } + + // Not in type_decls, check if it's an exposed item from an imported module + if (self.scopeLookupExposedItem(type_name_ident)) |exposed_info| { + const module_name_text = self.env.getIdent(exposed_info.module_name); + if (self.scopeLookupImportedModule(module_name_text)) |import_idx| { + // Get the node index from the imported module + if (self.module_envs) |envs_map| { + if (envs_map.get(exposed_info.module_name)) |auto_imported_type| { + // Convert identifier from current module to target module's interner + const original_name_text = self.env.getIdent(exposed_info.original_name); + const target_ident = auto_imported_type.env.common.findIdent(original_name_text) orelse { + // Type identifier doesn't exist in the target module + return try self.env.pushMalformed(TypeAnno.Idx, CIR.Diagnostic{ .type_not_exposed = .{ + .module_name = exposed_info.module_name, + .type_name = type_name_ident, + .region = type_name_region, + } }); + }; + const target_node_idx = auto_imported_type.env.getExposedNodeIndexById(target_ident) orelse { + // Type is not exposed by the imported module + return try self.env.pushMalformed(TypeAnno.Idx, CIR.Diagnostic{ .type_not_exposed = .{ + .module_name = exposed_info.module_name, + .type_name = type_name_ident, + .region = type_name_region, + } }); + }; + return try self.env.addTypeAnno(CIR.TypeAnno{ .lookup = .{ + .name = type_name_ident, + .base = .{ .external = .{ + .module_idx = import_idx, + .target_node_idx = target_node_idx, + } }, + } }, region); + } + } + } + } + + // Check if this is a type variable in scope (e.g., R1, R2 from requires { R1, R2 }) + switch (self.scopeLookupTypeVar(type_name_ident)) { + .found => |found_anno_idx| { + // Found a type variable with this name - create a reference to it + return try self.env.addTypeAnno(.{ .rigid_var_lookup = .{ + .ref = found_anno_idx, + } }, region); + }, + .not_found => {}, + } + + // Not found anywhere - undeclared type + return try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .undeclared_type = .{ .name = type_name_ident, .region = type_name_region, } }); - return .{ .anno_idx = try self.env.addTypeAnnoAndTypeVar(.{ .ty = .{ - .symbol = type_name_ident, - } }, .err, region), .mb_local_decl_idx = null }; - }; - - const type_decl = self.env.store.getStatement(type_decl_idx); - switch (type_decl) { - .s_alias_decl => |_| { - return .{ - .anno_idx = try self.env.addTypeAnnoAndTypeVarRedirect(CIR.TypeAnno{ .ty = .{ - .symbol = type_name_ident, - } }, ModuleEnv.varFrom(type_decl_idx), region), - .mb_local_decl_idx = type_decl_idx, - }; - }, - .s_nominal_decl => |_| { - return .{ - .anno_idx = try self.env.addTypeAnnoAndTypeVarRedirect(CIR.TypeAnno{ .ty = .{ - .symbol = type_name_ident, - } }, ModuleEnv.varFrom(type_decl_idx), region), - .mb_local_decl_idx = type_decl_idx, - }; - }, - else => { - // Since we looked up this type decl from `scopeLookupTypeDecl` - // this state should be impossible - unreachable; - }, } } else { - // This is an external type - - // Get the fully resolved module name + // First, check if this is a qualified name for an associated type (e.g., Foo.Bar) + // Build the full qualified name const strip_tokens = [_]tokenize.Token.Tag{.NoSpaceDotUpperIdent}; + const qualified_prefix = self.parse_ir.resolveQualifiedName( + ty.qualifiers, + ty.token, + &strip_tokens, + ); + const qualified_name_ident = try self.env.insertIdent(base.Ident.for_text(qualified_prefix)); + + // Try looking up the full qualified name in local scope (for associated types) + if (self.scopeLookupTypeDecl(qualified_name_ident)) |type_decl_idx| { + return try self.env.addTypeAnno(CIR.TypeAnno{ .lookup = .{ + .name = qualified_name_ident, + .base = .{ .local = .{ .decl_idx = type_decl_idx } }, + } }, region); + } + + // Not a local qualified type, so treat as an external type from a module + // Get qualifiers excluding the last one for module alias + const module_qualifiers: AST.Token.Span = if (qualifier_toks.len > 1) + .{ .span = .{ .start = ty.qualifiers.span.start, .len = @intCast(qualifier_toks.len - 1) } } + else + .{ .span = .{ .start = 0, .len = 0 } }; + const module_alias_text = self.parse_ir.resolveQualifiedName( - .{ .span = .{ .start = 0, .len = @intCast(qualifier_toks.len - 1) } }, + module_qualifiers, qualifier_toks[qualifier_toks.len - 1], &strip_tokens, ); const module_alias = try self.env.insertIdent(base.Ident.for_text(module_alias_text)); // Check if this is a module alias - const module_name = self.scopeLookupModule(module_alias) orelse { - // Module is not in current scope - return .{ .anno_idx = try self.env.pushMalformed(TypeAnno.Idx, CIR.Diagnostic{ .module_not_imported = .{ + const module_info = self.scopeLookupModule(module_alias) orelse { + // Module is not in current scope - but check if it's a type name first + if (self.scopeLookupTypeBinding(module_alias)) |_| { + // This is in scope as a type/value, but doesn't expose the nested type being requested + return try self.env.pushMalformed(TypeAnno.Idx, CIR.Diagnostic{ .nested_type_not_found = .{ + .parent_name = module_alias, + .nested_name = type_name_ident, + .region = region, + } }); + } + + // Not a module and not a type - module not imported + return try self.env.pushMalformed(TypeAnno.Idx, CIR.Diagnostic{ .module_not_imported = .{ .module_name = module_alias, .region = region, - } }), .mb_local_decl_idx = null }; + } }); }; + const module_name = module_info.module_name; const module_name_text = self.env.getIdent(module_name); // Check if this module is imported in the current scope const import_idx = self.scopeLookupImportedModule(module_name_text) orelse { - return .{ .anno_idx = try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .module_not_imported = .{ + return try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .module_not_imported = .{ .module_name = module_name, .region = region, - } }), .mb_local_decl_idx = null }; + } }); }; // Look up the target node index in the module's exposed_nodes const type_name_text = self.env.getIdent(type_name_ident); - const target_node_idx, const type_content = blk: { + const target_node_idx = blk: { const envs_map = self.module_envs orelse { - break :blk .{ 0, Content.err }; + break :blk 0; }; - const module_env = envs_map.get(module_name_text) orelse { - break :blk .{ 0, Content.err }; + const auto_imported_type = envs_map.get(module_name) orelse { + break :blk 0; }; - const target_ident = module_env.common.findIdent(type_name_text) orelse { + const target_ident = auto_imported_type.env.common.findIdent(type_name_text) orelse { // Type is not exposed by the module - return .{ .anno_idx = try self.env.pushMalformed(TypeAnno.Idx, CIR.Diagnostic{ .type_not_exposed = .{ + return try self.env.pushMalformed(TypeAnno.Idx, CIR.Diagnostic{ .type_not_exposed = .{ .module_name = module_name, .type_name = type_name_ident, .region = type_name_region, - } }), .mb_local_decl_idx = null }; + } }); }; - const other_module_node_id = module_env.getExposedNodeIndexById(target_ident) orelse { + const other_module_node_id = auto_imported_type.env.getExposedNodeIndexById(target_ident) orelse { // Type is not exposed by the module - return .{ .anno_idx = try self.env.pushMalformed(TypeAnno.Idx, CIR.Diagnostic{ .type_not_exposed = .{ + return try self.env.pushMalformed(TypeAnno.Idx, CIR.Diagnostic{ .type_not_exposed = .{ .module_name = module_name, .type_name = type_name_ident, .region = type_name_region, - } }), .mb_local_decl_idx = null }; + } }); }; // Successfully found the target node - break :blk .{ other_module_node_id, Content{ .flex_var = null } }; + break :blk other_module_node_id; }; // Create the ty_lookup_external expression with Import.Idx // Type solving will copy this types from the origin type store into the // this module's type store - return .{ - .anno_idx = try self.env.addTypeAnnoAndTypeVar(CIR.TypeAnno{ .ty_lookup_external = .{ - .module_idx = import_idx, - .target_node_idx = target_node_idx, - } }, type_content, region), - .mb_local_decl_idx = null, - }; + return try self.env.addTypeAnno(CIR.TypeAnno{ .lookup = .{ .name = type_name_ident, .base = .{ .external = .{ + .module_idx = import_idx, + .target_node_idx = target_node_idx, + } } } }, region); } } @@ -5203,7 +8271,7 @@ fn canonicalizeTypeAnnoTypeApplication( // Canonicalize the base type first const based_anno_ast = self.parse_ir.store.getTypeAnno(args_slice[0]); - const base_canonicalized = blk: { + const base_anno_idx = blk: { switch (based_anno_ast) { .ty => |ty| { break :blk try self.canonicalizeTypeAnnoBasicType(ty); @@ -5213,7 +8281,7 @@ fn canonicalizeTypeAnnoTypeApplication( }, } }; - const base_anno = self.env.store.getTypeAnno(base_canonicalized.anno_idx); + const base_anno = self.env.store.getTypeAnno(base_anno_idx); // Canonicalize type arguments (skip first which is the type name) const scratch_top = self.env.store.scratchTypeAnnoTop(); @@ -5229,45 +8297,21 @@ fn canonicalizeTypeAnnoTypeApplication( // Then, we must instantiate the type from the base declaration *with* the // user-provided type arugmuments applied switch (base_anno) { - .ty => |ty| { + .lookup => |ty| { if (type_anno_ctx.isTypeDeclAndHasUnderscore()) { - return try self.env.addTypeAnnoAndTypeVar(.{ .apply = .{ - .symbol = ty.symbol, - .args = args_span, - } }, .err, region); + try self.env.pushDiagnostic(Diagnostic{ .underscore_in_type_declaration = .{ + .is_alias = true, + .region = self.env.store.getTypeAnnoRegion(base_anno_idx), + } }); } - const local_decl_idx = base_canonicalized.mb_local_decl_idx orelse { - return try self.env.addTypeAnnoAndTypeVar(.{ .apply = .{ - .symbol = ty.symbol, - .args = args_span, - } }, .err, region); - }; - - return try self.env.addTypeAnnoAndTypeVarRedirect(.{ .apply = .{ - .symbol = ty.symbol, + return try self.env.addTypeAnno(.{ .apply = .{ + .name = ty.name, + .base = ty.base, .args = args_span, - } }, ModuleEnv.varFrom(local_decl_idx), region); + } }, region); }, - .ty_lookup_external => |tle| { - if (type_anno_ctx.isTypeDeclAndHasUnderscore()) { - return try self.env.addTypeAnnoAndTypeVar(.{ .apply_external = .{ - .module_idx = tle.module_idx, - .target_node_idx = tle.target_node_idx, - .args = args_span, - } }, .err, region); - } else { - // Set the type to be flex var for now. The type solving phase - // will copy the type from the original module's type store into - // this module's type store - return try self.env.addTypeAnnoAndTypeVar(.{ .apply_external = .{ - .module_idx = tle.module_idx, - .target_node_idx = tle.target_node_idx, - .args = args_span, - } }, .{ .flex_var = null }, region); - } - }, - else => return base_canonicalized.anno_idx, + else => return base_anno_idx, } } @@ -5300,22 +8344,13 @@ fn canonicalizeTypeAnnoTuple( try self.env.store.addScratchTypeAnno(canonicalized_elem_idx); const elem_var = ModuleEnv.varFrom(canonicalized_elem_idx); - try self.scratch_vars.append(self.env.gpa, elem_var); + try self.scratch_vars.append(elem_var); } const annos = try self.env.store.typeAnnoSpanFrom(scratch_top); - const content = blk: { - if (type_anno_ctx.isTypeDeclAndHasUnderscore()) { - break :blk types.Content{ .err = {} }; - } else { - const elems_var_range = try self.env.types.appendVars(self.scratch_vars.sliceFromStart(scratch_vars_top)); - break :blk types.Content{ .structure = FlatType{ .tuple = .{ .elems = elems_var_range } } }; - } - }; - - return try self.env.addTypeAnnoAndTypeVar(.{ .tuple = .{ + return try self.env.addTypeAnno(.{ .tuple = .{ .elems = annos, - } }, content, region); + } }, region); } } @@ -5352,14 +8387,14 @@ fn canonicalizeTypeAnnoRecord( .name = malformed_ident, .ty = canonicalized_ty, }; - const field_cir_idx = try self.env.addAnnoRecordFieldAndTypeVarRedirect( + const field_cir_idx = try self.env.addAnnoRecordField( cir_field, - ModuleEnv.varFrom(canonicalized_ty), + self.parse_ir.tokenizedRegionToRegion(ast_field.region), ); try self.env.store.addScratchAnnoRecordField(field_cir_idx); - try self.scratch_record_fields.append(self.env.gpa, types.RecordField{ + try self.scratch_record_fields.append(types.RecordField{ .name = malformed_ident, .var_ = ModuleEnv.varFrom(field_cir_idx), }); @@ -5375,14 +8410,14 @@ fn canonicalizeTypeAnnoRecord( .name = field_name, .ty = canonicalized_ty, }; - const field_cir_idx = try self.env.addAnnoRecordFieldAndTypeVarRedirect( + const field_cir_idx = try self.env.addAnnoRecordField( cir_field, - ModuleEnv.varFrom(canonicalized_ty), + self.parse_ir.tokenizedRegionToRegion(ast_field.region), ); try self.env.store.addScratchAnnoRecordField(field_cir_idx); - try self.scratch_record_fields.append(self.env.gpa, types.RecordField{ + try self.scratch_record_fields.append(types.RecordField{ .name = field_name, .var_ = ModuleEnv.varFrom(field_cir_idx), }); @@ -5393,27 +8428,16 @@ fn canonicalizeTypeAnnoRecord( // Should we be sorting here? const record_fields_scratch = self.scratch_record_fields.sliceFromStart(scratch_record_fields_top); std.mem.sort(types.RecordField, record_fields_scratch, self.env.common.getIdentStore(), comptime types.RecordField.sortByNameAsc); - const fields_type_range = try self.env.types.appendRecordFields(record_fields_scratch); - const content = blk: { - if (type_anno_ctx.isTypeDeclAndHasUnderscore()) { - break :blk types.Content{ .err = {} }; - } else { - // TODO: Add parser support for extensible variables in - // record then thread that through here - const ext_var = try self.env.addTypeSlotAndTypeVar( - @enumFromInt(0), // TODO - .{ .structure = .empty_record }, - region, - TypeVar, - ); - break :blk Content{ .structure = .{ .record = .{ .fields = fields_type_range, .ext = ext_var } } }; - } - }; + // Canonicalize the extension, if it exists + const mb_ext_anno = if (record.ext) |ext_idx| blk: { + break :blk try self.canonicalizeTypeAnnoHelp(ext_idx, type_anno_ctx); + } else null; - return try self.env.addTypeAnnoAndTypeVar(.{ .record = .{ + return try self.env.addTypeAnno(.{ .record = .{ .fields = field_anno_idxs, - } }, content, region); + .ext = mb_ext_anno, + } }, region); } /// Handle tag union types like [Some(a), None] @@ -5431,73 +8455,24 @@ fn canonicalizeTypeAnnoTagUnion( const scratch_annos_top = self.env.store.scratchTypeAnnoTop(); defer self.env.store.clearScratchTypeAnnosFrom(scratch_annos_top); - const scratch_tags_top = self.scratch_tags.top(); - defer self.scratch_tags.clearFrom(scratch_tags_top); - for (self.parse_ir.store.typeAnnoSlice(tag_union.tags)) |tag_idx| { - // First canonicalized the tag variant + // Canonicalized the tag variant // This will always return a `ty` or an `apply` const canonicalized_tag_idx = try self.canonicalizeTypeAnnoTag(tag_idx, type_anno_ctx); try self.env.store.addScratchTypeAnno(canonicalized_tag_idx); - - // Then, create the type system tag and append to scratch tags - const tag_cir_anno = self.env.store.getTypeAnno(canonicalized_tag_idx); - const tag = blk: { - switch (tag_cir_anno) { - .ty => |ty| { - break :blk try self.env.types.mkTag(ty.symbol, &.{}); - }, - .apply => |apply| { - const args_slice: []TypeVar = @ptrCast(self.env.store.sliceTypeAnnos(apply.args)); - break :blk try self.env.types.mkTag(apply.symbol, args_slice); - }, - .malformed => { - continue; - }, - else => unreachable, - } - }; - try self.scratch_tags.append(self.env.gpa, tag); } const tag_anno_idxs = try self.env.store.typeAnnoSpanFrom(scratch_annos_top); - // Should we be sorting here? - const tags_slice = self.scratch_tags.sliceFromStart(scratch_tags_top); - std.mem.sort(types.Tag, tags_slice, self.env.common.getIdentStore(), comptime types.Tag.sortByNameAsc); - // Canonicalize the ext, if it exists const mb_ext_anno = if (tag_union.open_anno) |open_idx| blk: { break :blk try self.canonicalizeTypeAnnoHelp(open_idx, type_anno_ctx); } else null; - const content = blk: { - if (type_anno_ctx.isTypeDeclAndHasUnderscore()) { - break :blk types.Content{ .err = {} }; - } else { - // Make the ext type variable - const ext_var = inner_blk: { - if (mb_ext_anno) |ext_anno| { - break :inner_blk ModuleEnv.varFrom(ext_anno); - } else { - break :inner_blk try self.env.addTypeSlotAndTypeVar( - @enumFromInt(0), - .{ .structure = .empty_tag_union }, - region, - TypeVar, - ); - } - }; - - // Make type system tag union - break :blk try self.env.types.mkTagUnion(tags_slice, ext_var); - } - }; - - return try self.env.addTypeAnnoAndTypeVar(.{ .tag_union = .{ + return try self.env.addTypeAnno(.{ .tag_union = .{ .tags = tag_anno_idxs, .ext = mb_ext_anno, - } }, content, region); + } }, region); } /// Canonicalize a tag variant within a tag union type annotation @@ -5525,9 +8500,10 @@ fn canonicalizeTypeAnnoTag( // Create identifier from text if resolution fails try self.env.insertIdent(base.Ident.for_text(self.parse_ir.resolve(ty.token))); - return try self.env.addTypeAnnoAndTypeVar(.{ .ty = .{ - .symbol = ident_idx, - } }, Content{ .flex_var = null }, region); + return try self.env.addTypeAnno(.{ .tag = .{ + .name = ident_idx, + .args = .{ .span = DataSpan.empty() }, + } }, region); }, .apply => |apply| { // For tags with arguments like `Some(Str)`, validate the arguments but not the tag name @@ -5559,10 +8535,10 @@ fn canonicalizeTypeAnnoTag( } const args = try self.env.store.typeAnnoSpanFrom(scratch_top); - return try self.env.addTypeAnnoAndTypeVar(.{ .apply = .{ - .symbol = type_name, + return try self.env.addTypeAnno(.{ .tag = .{ + .name = type_name, .args = args, - } }, Content{ .flex_var = null }, region); + } }, region); }, else => { return try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ @@ -5589,35 +8565,19 @@ fn canonicalizeTypeAnnoFunc( const args_span = try self.env.store.typeAnnoSpanFrom(scratch_top); - const args_anno_idxs = self.env.store.sliceTypeAnnos(args_span); - const args_vars: []TypeVar = @ptrCast(@alignCast(args_anno_idxs)); - // Canonicalize return type const ret_anno_idx = try self.canonicalizeTypeAnnoHelp(func.ret, type_anno_ctx); - const ret_var = ModuleEnv.varFrom(ret_anno_idx); - const content = blk: { - if (type_anno_ctx.isTypeDeclAndHasUnderscore()) { - break :blk types.Content{ .err = {} }; - } else { - if (func.effectful) { - break :blk try self.env.types.mkFuncEffectful(args_vars, ret_var); - } else { - break :blk try self.env.types.mkFuncPure(args_vars, ret_var); - } - } - }; - - return try self.env.addTypeAnnoAndTypeVar(.{ .@"fn" = .{ + return try self.env.addTypeAnno(.{ .@"fn" = .{ .args = args_span, .ret = ret_anno_idx, .effectful = func.effectful, - } }, content, region); + } }, region); } //////////////////////////////////////////////////////////////////////////////// -fn canonicalizeTypeHeader(self: *Self, header_idx: AST.TypeHeader.Idx) std.mem.Allocator.Error!CIR.TypeHeader.Idx { +fn canonicalizeTypeHeader(self: *Self, header_idx: AST.TypeHeader.Idx, type_kind: AST.TypeDeclKind) std.mem.Allocator.Error!CIR.TypeHeader.Idx { const trace = tracy.trace(@src()); defer trace.end(); @@ -5625,11 +8585,10 @@ fn canonicalizeTypeHeader(self: *Self, header_idx: AST.TypeHeader.Idx) std.mem.A const node = self.parse_ir.store.nodes.get(@enumFromInt(@intFromEnum(header_idx))); const node_region = self.parse_ir.tokenizedRegionToRegion(node.region); if (node.tag == .malformed) { - // Create a malformed type header with an invalid identifier - return try self.env.addTypeHeaderAndTypeVar(.{ - .name = base.Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 0 }, // Invalid identifier - .args = .{ .span = .{ .start = 0, .len = 0 } }, - }, Content{ .flex_var = null }, node_region); + // Create a malformed type header node that will be caught by processTypeDeclFirstPass + return try self.env.pushMalformed(CIR.TypeHeader.Idx, Diagnostic{ .malformed_type_annotation = .{ + .region = node_region, + } }); } const ast_header = self.parse_ir.store.getTypeHeader(header_idx) catch unreachable; // Malformed handled above @@ -5637,13 +8596,26 @@ fn canonicalizeTypeHeader(self: *Self, header_idx: AST.TypeHeader.Idx) std.mem.A // Get the type name identifier const name_ident = self.parse_ir.tokens.resolveIdentifier(ast_header.name) orelse { - // If we can't resolve the identifier, create a malformed header with invalid identifier - return try self.env.addTypeHeaderAndTypeVar(.{ - .name = base.Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 0 }, // Invalid identifier - .args = .{ .span = .{ .start = 0, .len = 0 } }, - }, Content{ .flex_var = null }, region); + // If we can't resolve the identifier, create a malformed header node + return try self.env.pushMalformed(CIR.TypeHeader.Idx, Diagnostic{ .malformed_type_annotation = .{ + .region = region, + } }); }; + // Check if this is a builtin type + // Allow builtin type names to be redeclared in the Builtin module + // (e.g., Str := ... within Builtin.roc) + // Use identifier index comparison instead of string comparison for efficiency + if (TypeAnno.Builtin.isBuiltinTypeIdent(name_ident, self.env.idents)) { + const is_builtin_module = std.mem.eql(u8, self.env.module_name, "Builtin"); + if (!is_builtin_module) { + return try self.env.pushMalformed(CIR.TypeHeader.Idx, Diagnostic{ .ident_already_in_scope = .{ + .ident = name_ident, + .region = region, + } }); + } + } + // Canonicalize type arguments - these are parameter declarations, not references const scratch_top = self.env.store.scratchTypeAnnoTop(); defer self.env.store.clearScratchTypeAnnosFrom(scratch_top); @@ -5664,17 +8636,43 @@ fn canonicalizeTypeHeader(self: *Self, header_idx: AST.TypeHeader.Idx) std.mem.A // Create type variable annotation for this parameter // Check for underscore in type parameter + // Only reject underscore-prefixed names for type aliases, not nominal/opaque types const param_name = self.parse_ir.env.getIdent(param_ident); - if (param_name.len > 0 and param_name[0] == '_') { + if (param_name.len > 0 and param_name[0] == '_' and type_kind == .alias) { try self.env.pushDiagnostic(Diagnostic{ .underscore_in_type_declaration = .{ .is_alias = true, .region = param_region, } }); } - const param_anno = try self.env.addTypeAnnoAndTypeVar(.{ .ty_var = .{ + const param_anno = try self.env.addTypeAnno(.{ .rigid_var = .{ .name = param_ident, - } }, Content{ .rigid_var = param_ident }, param_region); + } }, param_region); + try self.env.store.addScratchTypeAnno(param_anno); + }, + .underscore_type_var => |underscore_ty_var| { + // Handle underscore-prefixed type parameters like _a, _foo + const param_region = self.parse_ir.tokenizedRegionToRegion(underscore_ty_var.region); + const param_ident = self.parse_ir.tokens.resolveIdentifier(underscore_ty_var.tok) orelse { + const malformed = try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .malformed_type_annotation = .{ + .region = param_region, + } }); + try self.env.store.addScratchTypeAnno(malformed); + continue; + }; + + // Only reject underscore-prefixed parameters for type aliases, not nominal/opaque types + if (type_kind == .alias) { + try self.env.pushDiagnostic(Diagnostic{ .underscore_in_type_declaration = .{ + .is_alias = true, + .region = param_region, + } }); + } + + // Create rigid variable for this parameter + const param_anno = try self.env.addTypeAnno(.{ .rigid_var = .{ + .name = param_ident, + } }, param_region); try self.env.store.addScratchTypeAnno(param_anno); }, .underscore => |underscore_param| { @@ -5682,13 +8680,16 @@ fn canonicalizeTypeHeader(self: *Self, header_idx: AST.TypeHeader.Idx) std.mem.A const param_region = self.parse_ir.tokenizedRegionToRegion(underscore_param.region); // Push underscore diagnostic for underscore type parameters - try self.env.pushDiagnostic(Diagnostic{ .underscore_in_type_declaration = .{ - .is_alias = true, - .region = param_region, - } }); + // Only reject for type aliases, not nominal/opaque types + if (type_kind == .alias) { + try self.env.pushDiagnostic(Diagnostic{ .underscore_in_type_declaration = .{ + .is_alias = true, + .region = param_region, + } }); + } // Create underscore type annotation - const underscore_anno = try self.env.addTypeAnnoAndTypeVar(.{ .underscore = {} }, Content{ .err = {} }, param_region); + const underscore_anno = try self.env.addTypeAnno(.{ .underscore = {} }, param_region); try self.env.store.addScratchTypeAnno(underscore_anno); }, .malformed => |malformed_param| { @@ -5696,10 +8697,13 @@ fn canonicalizeTypeHeader(self: *Self, header_idx: AST.TypeHeader.Idx) std.mem.A const param_region = self.parse_ir.tokenizedRegionToRegion(malformed_param.region); // Push underscore diagnostic for malformed underscore type parameters - try self.env.pushDiagnostic(Diagnostic{ .underscore_in_type_declaration = .{ - .is_alias = true, - .region = param_region, - } }); + // Only reject for type aliases, not nominal/opaque types + if (type_kind == .alias) { + try self.env.pushDiagnostic(Diagnostic{ .underscore_in_type_declaration = .{ + .is_alias = true, + .region = param_region, + } }); + } // Create malformed type annotation using pushMalformed for consistency const malformed_anno = try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .malformed_type_annotation = .{ @@ -5708,186 +8712,262 @@ fn canonicalizeTypeHeader(self: *Self, header_idx: AST.TypeHeader.Idx) std.mem.A try self.env.store.addScratchTypeAnno(malformed_anno); }, else => { - // Other types in parameter position - canonicalize normally but warn - try self.env.pushDiagnostic(Diagnostic{ .malformed_type_annotation = .{ + const malformed_anno = try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .malformed_type_annotation = .{ .region = node_region, } }); - const canonicalized = try self.canonicalizeTypeAnno(arg_idx, .type_decl_anno); - try self.env.store.addScratchTypeAnno(canonicalized); + try self.env.store.addScratchTypeAnno(malformed_anno); }, } } const args = try self.env.store.typeAnnoSpanFrom(scratch_top); - return try self.env.addTypeHeaderAndTypeVar(.{ + // For original headers from parsing, relative_name is the same as name + // (it will be differentiated when a qualified header is created in processTypeDeclFirstPass) + return try self.env.addTypeHeader(.{ .name = name_ident, + .relative_name = name_ident, .args = args, - }, Content{ .flex_var = null }, region); + }, region); } // expr statements // -// A canonicalized statement -const CanonicalizedStatement = struct { - idx: Statement.Idx, - free_vars: ?[]Pattern.Idx, -}; +fn canonicalizeBlock(self: *Self, e: AST.Block) std.mem.Allocator.Error!CanonicalizedExpr { + const block_region = self.parse_ir.tokenizedRegionToRegion(e.region); -/// A statement type annotation -pub const StmtTypeAnno = struct { - anno_idx: Statement.Idx, - anno: std.meta.FieldType(Statement, .s_type_anno), -}; + // Blocks don't introduce function boundaries, but may contain var statements + try self.scopeEnter(self.env.gpa, false); // false = not a function boundary + defer self.scopeExit(self.env.gpa) catch {}; -// The result of canonicalizing a statement -const CanonicalizedStatementResult = union(enum) { - import_stmt, - stmt: CanonicalizedStatement, -}; + // Statements inside a block are in statement position. + // This is important for constructs like `if` without `else`, which are only + // valid in statement position (where their value is not used). + const saved_stmt_pos = self.in_statement_position; + self.in_statement_position = true; + defer self.in_statement_position = saved_stmt_pos; -/// Canonicalize a statement in the canonical IR. -/// -/// This always succeed, but the Statement.Idx returned may be null only if the -/// statement was an imported. -pub fn canonicalizeStatement( - self: *Self, - ast_stmt_idx: AST.Statement.Idx, - last_type_anno: *?StmtTypeAnno, -) std.mem.Allocator.Error!CanonicalizedStatementResult { - const trace = tracy.trace(@src()); - defer trace.end(); + // Keep track of the start position for statements + const stmt_start = self.env.store.scratch.?.statements.top(); - // In many of these branches, we defer setting `last_anno_type` to ensure - // that in the case of early returns, the value is reset properly. - // - // We can't have the `defer` outide the switch branches because not all - // branches should reset `last_type_anno` + // Track bound variables using scratch space (for filtering out locally-bound vars from captures) + const bound_vars_top = self.scratch_bound_vars.top(); + defer self.scratch_bound_vars.clearFrom(bound_vars_top); - const ast_stmt = self.parse_ir.store.getStatement(ast_stmt_idx); - switch (ast_stmt) { - .decl => |d| { - defer last_type_anno.* = null; // See above comment for why this is necessary - const region = self.parse_ir.tokenizedRegionToRegion(d.region); + const captures_top = self.scratch_captures.top(); + defer self.scratch_captures.clearFrom(captures_top); - // Check if this is a var reassignment - const ast_pattern = self.parse_ir.store.getPattern(d.pattern); - switch (ast_pattern) { - .ident => |pattern_ident| { - const ident_region = self.parse_ir.tokenizedRegionToRegion(pattern_ident.region); - const ident_tok = pattern_ident.ident_tok; + // Canonicalize all statements in the block + const ast_stmt_idxs = self.parse_ir.store.statementSlice(e.statements); + var last_expr: ?CanonicalizedExpr = null; - if (self.parse_ir.tokens.resolveIdentifier(ident_tok)) |ident_idx| { - // Check if this identifier exists and is a var - switch (self.scopeLookup(.ident, ident_idx)) { - .found => |existing_pattern_idx| { - // Check if this is a var reassignment across function boundaries - if (self.isVarReassignmentAcrossFunctionBoundary(existing_pattern_idx)) { - // Generate error for var reassignment across function boundary - const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .var_across_function_boundary = .{ - .region = ident_region, - } }); + var i: u32 = 0; + while (i < ast_stmt_idxs.len) : (i += 1) { + const ast_stmt_idx = ast_stmt_idxs[i]; + const ast_stmt = self.parse_ir.store.getStatement(ast_stmt_idx); - // Create a reassign statement with the error expression - const reassign_idx = try self.env.addStatementAndTypeVarRedirect(Statement{ .s_reassign = .{ - .pattern_idx = existing_pattern_idx, - .expr = malformed_idx, - } }, ModuleEnv.varFrom(malformed_idx), ident_region); - - return .{ - .stmt = CanonicalizedStatement{ .idx = reassign_idx, .free_vars = null }, - }; - } - - // Check if this was declared as a var - if (self.isVarPattern(existing_pattern_idx)) { - // This is a var reassignment - canonicalize the expression and create reassign statement - const expr = try self.canonicalizeExprOrMalformed(d.body); - - // Create reassign statement - const reassign_idx = try self.env.addStatementAndTypeVarRedirect(Statement{ .s_reassign = .{ - .pattern_idx = existing_pattern_idx, - .expr = expr.idx, - } }, ModuleEnv.varFrom(expr.idx), ident_region); - - return .{ - .stmt = CanonicalizedStatement{ .idx = reassign_idx, .free_vars = expr.free_vars }, - }; - } - }, - .not_found => { - // Not found in scope, fall through to regular declaration - }, - } - } + // Check if this is the last statement and if it's an expression + const is_last = (i == ast_stmt_idxs.len - 1); + if (is_last and (ast_stmt == .expr or ast_stmt == .dbg or ast_stmt == .@"return" or ast_stmt == .crash)) { + // If the last statement is expr, debg, return or crash, then we + // canonicalize the expr directly without adding it as a statement + switch (ast_stmt) { + .expr => |expr_stmt| { + last_expr = try self.canonicalizeExprOrMalformed(expr_stmt.expr); }, - else => {}, - } + .dbg => |dbg_stmt| { + // For final debug statements, canonicalize as debug expression + const debug_region = self.parse_ir.tokenizedRegionToRegion(dbg_stmt.region); + const inner_expr = try self.canonicalizeExprOrMalformed(dbg_stmt.expr); - // check against last anno + // Create debug expression + const dbg_expr = try self.env.addExpr(Expr{ .e_dbg = .{ + .expr = inner_expr.idx, + } }, debug_region); + last_expr = CanonicalizedExpr{ .idx = dbg_expr, .free_vars = inner_expr.free_vars }; + }, + .@"return" => |return_stmt| { + // Create an e_return expression to preserve early return semantics + // This is for when return is the final expression in a block + const inner_expr = try self.canonicalizeExprOrMalformed(return_stmt.expr); + const return_region = self.parse_ir.tokenizedRegionToRegion(return_stmt.region); + const return_expr_idx = try self.env.addExpr(Expr{ .e_return = .{ + .expr = inner_expr.idx, + } }, return_region); + last_expr = CanonicalizedExpr{ .idx = return_expr_idx, .free_vars = inner_expr.free_vars }; + }, + .crash => |crash_stmt| { + // For final debug statements, canonicalize as debug expression + const crash_region = self.parse_ir.tokenizedRegionToRegion(crash_stmt.region); - // Regular declaration - canonicalize as usual - const pattern_idx = try self.canonicalizePattern(d.pattern) orelse blk: { - const pattern = self.parse_ir.store.getPattern(d.pattern); - break :blk try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .expr_not_canonicalized = .{ - .region = self.parse_ir.tokenizedRegionToRegion(pattern.to_tokenized_region()), - } }); - }; - - const expr = try self.canonicalizeExprOrMalformed(d.body); - - // Check if this declaration matches the last type annotation - var annotation_idx: ?Annotation.Idx = null; - if (last_type_anno.*) |anno_info| { - if (ast_pattern == .ident) { - const pattern_ident = ast_pattern.ident; - if (self.parse_ir.tokens.resolveIdentifier(pattern_ident.ident_tok)) |decl_ident| { - if (anno_info.anno.name.idx == decl_ident.idx) { - // This declaration matches the type annotation - const pattern_region = self.parse_ir.tokenizedRegionToRegion(ast_pattern.ident.region); - annotation_idx = try self.createAnnotationFromTypeAnno(anno_info.anno.anno, pattern_region); - - // Clear the annotation since we've used it - last_type_anno.* = null; + // Create crash expression + // Extract string content from the crash expression or create malformed if not string + const crash_expr = blk: { + const msg_expr = self.parse_ir.store.getExpr(crash_stmt.expr); + switch (msg_expr) { + .string => |s| { + // For string literals, we need to extract the actual string parts + const parts = self.parse_ir.store.exprSlice(s.parts); + if (parts.len > 0) { + const first_part = self.parse_ir.store.getExpr(parts[0]); + if (first_part == .string_part) { + const part_text = self.parse_ir.resolve(first_part.string_part.token); + break :blk try self.env.addExpr(Expr{ .e_crash = .{ + .msg = try self.env.insertString(part_text), + } }, crash_region); + } + } + // Fall back to default if we can't extract + break :blk try self.env.addExpr(Expr{ .e_crash = .{ + .msg = try self.env.insertString("crash"), + } }, crash_region); + }, + else => { + // For non-string expressions, create a malformed expression + break :blk try self.env.pushMalformed(Expr.Idx, Diagnostic{ .crash_expects_string = .{ + .region = block_region, + } }); + }, } + }; + + last_expr = CanonicalizedExpr{ .idx = crash_expr, .free_vars = DataSpan.empty() }; + }, + else => unreachable, + } + } else { + // Otherwise, this is a normal statement + // + // We process each stmt individually, saving the result in + // mb_canonicailzed_stmt for post-processing + + const stmt_result = try self.canonicalizeBlockStatement(ast_stmt, ast_stmt_idxs, i); + + // Post processing for the stmt + if (stmt_result.canonicalized_stmt) |canonicailzed_stmt| { + try self.env.store.addScratchStatement(canonicailzed_stmt.idx); + + // Collect bound variables for the block + const cir_stmt = self.env.store.getStatement(canonicailzed_stmt.idx); + switch (cir_stmt) { + .s_decl => |decl| try self.collectBoundVarsToScratch(decl.pattern), + .s_decl_gen => |decl| try self.collectBoundVarsToScratch(decl.pattern), + .s_var => |var_stmt| try self.collectBoundVarsToScratch(var_stmt.pattern_idx), + else => {}, + } + + // Collect free vars from the statement into the block's scratch space + const stmt_free_vars_slice = self.scratch_free_vars.sliceFromSpan(canonicailzed_stmt.free_vars); + for (stmt_free_vars_slice) |fv| { + if (!self.scratch_captures.contains(fv) and !self.scratch_bound_vars.containsFrom(bound_vars_top, fv)) { + try self.scratch_captures.append(fv); } } } - // Create a declaration statement - const stmt_idx = try self.env.addStatementAndTypeVarRedirect(Statement{ .s_decl = .{ - .pattern = pattern_idx, - .expr = expr.idx, - .anno = annotation_idx, - } }, ModuleEnv.varFrom(expr.idx), region); + // Check if we processed two stmts in one pass + // eg a type annotation & it's definition + switch (stmt_result.stmts_processed) { + .one => {}, + .two => { + // If so, then increment twice this pass + i += 1; + }, + } + } + } - return .{ - .stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }, - }; + // Determine the final expression + const final_expr = if (last_expr) |can_expr| can_expr else blk: { + // Empty block - create empty record + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_empty_record = .{}, + }, block_region); + break :blk CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.empty() }; + }; + + // Add free vars from the final expression to the block's scratch space + const final_expr_free_vars_slice = self.scratch_free_vars.sliceFromSpan(final_expr.free_vars); + for (final_expr_free_vars_slice) |fv| { + if (!self.scratch_captures.contains(fv) and !self.scratch_bound_vars.containsFrom(bound_vars_top, fv)) { + try self.scratch_captures.append(fv); + } + } + + // Get a slice of the captured vars in the block + const captures_slice = self.scratch_captures.sliceFromStart(captures_top); + + // Add the actual free variables (captures) to the parent's scratch space + const block_captures_start = self.scratch_free_vars.top(); + for (captures_slice) |ptrn_idx| { + try self.scratch_free_vars.append(ptrn_idx); + } + const block_free_vars = self.scratch_free_vars.spanFrom(block_captures_start); + + // Create statement span + const stmt_span = try self.env.store.statementSpanFrom(stmt_start); + + // Create and return block expression + const block_expr = CIR.Expr{ + .e_block = .{ + .stmts = stmt_span, + .final_expr = final_expr.idx, }, - .@"var" => |v| { - defer last_type_anno.* = null; // See above comment for why this is necessary + }; + const block_idx = try self.env.addExpr(block_expr, block_region); + + return CanonicalizedExpr{ .idx = block_idx, .free_vars = block_free_vars }; +} + +const StatementResult = struct { + canonicalized_stmt: ?CanonicalizedStatement, + stmts_processed: StatementsProcessed, +}; + +const StatementsProcessed = enum { one, two }; + +/// Canonicalize a single statement within a block +/// +/// This function generally processes 1 stmt, but in the case of type +/// annotations, it may ties the following declaration. In this case, the first +/// stmt is the anno & the second is the following decl +/// +/// The stmt may be null if: +/// * the stmt is an import statement, in which case it is processed but not +/// added to CIR +/// * it's a type annotation without a where clause, in which case the anno is +/// simply attached to decl node +pub fn canonicalizeBlockStatement(self: *Self, ast_stmt: AST.Statement, ast_stmt_idxs: []const AST.Statement.Idx, current_index: u32) std.mem.Allocator.Error!StatementResult { + var mb_canonicailzed_stmt: ?CanonicalizedStatement = null; + var stmts_processed: StatementsProcessed = .one; + + switch (ast_stmt) { + .decl => |d| { + mb_canonicailzed_stmt = try self.canonicalizeBlockDecl(d, null); + }, + .@"var" => |v| blk: { const region = self.parse_ir.tokenizedRegionToRegion(v.region); // Var declaration - handle specially with function boundary tracking const var_name = self.parse_ir.tokens.resolveIdentifier(v.name) orelse { const feature = try self.env.insertString("resolve var name"); - return .{ .stmt = CanonicalizedStatement{ + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = try self.env.pushMalformed(Statement.Idx, Diagnostic{ .not_implemented = .{ .feature = feature, .region = region, } }), - .free_vars = null, - } }; + .free_vars = DataSpan.empty(), + }; + break :blk; }; // Canonicalize the initial value const expr = try self.canonicalizeExprOrMalformed(v.body); // Create pattern for the var - const pattern_idx = try self.env.addPatternAndTypeVarRedirect( + const pattern_idx = try self.env.addPattern( Pattern{ .assign = .{ .ident = var_name } }, - ModuleEnv.varFrom(expr.idx), + region, ); @@ -5895,37 +8975,31 @@ pub fn canonicalizeStatement( _ = try self.scopeIntroduceVar(var_name, pattern_idx, region, true, Pattern.Idx); // Create var statement - const stmt_idx = try self.env.addStatementAndTypeVarRedirect(Statement{ .s_var = .{ + const stmt_idx = try self.env.addStatement(Statement{ .s_var = .{ .pattern_idx = pattern_idx, .expr = expr.idx, - } }, ModuleEnv.varFrom(expr.idx), region); + } }, region); - return .{ - .stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }, - }; + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }; }, - .expr => |e| { - defer last_type_anno.* = null; // See above comment for why this is necessary - const region = self.parse_ir.tokenizedRegionToRegion(e.region); + .expr => |e_| { + const region = self.parse_ir.tokenizedRegionToRegion(e_.region); // Expression statement - const expr = try self.canonicalizeExprOrMalformed(e.expr); + const expr = try self.canonicalizeExprOrMalformed(e_.expr); // Create expression statement - const stmt_idx = try self.env.addStatementAndTypeVarRedirect(Statement{ .s_expr = .{ + const stmt_idx = try self.env.addStatement(Statement{ .s_expr = .{ .expr = expr.idx, - } }, ModuleEnv.varFrom(expr.idx), region); + } }, region); - return .{ - .stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }, - }; + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }; }, .crash => |c| { - defer last_type_anno.* = null; // See above comment for why this is necessary const region = self.parse_ir.tokenizedRegionToRegion(c.region); // Extract string content from the crash expression or create malformed if not string - const msg_literal = blk: { + const mb_msg_literal = blk: { const msg_expr = self.parse_ir.store.getExpr(c.expr); switch (msg_expr) { .string => |s| { @@ -5942,28 +9016,28 @@ pub fn canonicalizeStatement( break :blk try self.env.insertString("crash"); }, else => { - // For non-string expressions, create a malformed expression - const malformed_idx = try self.env.pushMalformed(Statement.Idx, Diagnostic{ .crash_expects_string = .{ - .region = region, - } }); - return .{ - .stmt = CanonicalizedStatement{ .idx = malformed_idx, .free_vars = null }, - }; + break :blk null; }, } }; - // Create crash statement - const stmt_idx = try self.env.addStatementAndTypeVar(Statement{ .s_crash = .{ - .msg = msg_literal, - } }, .err, region); - - return .{ - .stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = null }, + const stmt_idx = blk: { + if (mb_msg_literal) |msg_literal| { + // Create crash statement + break :blk try self.env.addStatement(Statement{ .s_crash = .{ + .msg = msg_literal, + } }, region); + } else { + // For non-string expressions, create a malformed expression + break :blk try self.env.pushMalformed(Statement.Idx, Diagnostic{ .crash_expects_string = .{ + .region = region, + } }); + } }; + + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = DataSpan.empty() }; }, .dbg => |d| { - defer last_type_anno.* = null; // See above comment for why this is necessary const region = self.parse_ir.tokenizedRegionToRegion(d.region); // Canonicalize the debug expression @@ -5971,62 +9045,185 @@ pub fn canonicalizeStatement( // Create dbg statement - const stmt_idx = try self.env.addStatementAndTypeVarRedirect(Statement{ .s_dbg = .{ + const stmt_idx = try self.env.addStatement(Statement{ .s_dbg = .{ .expr = expr.idx, - } }, ModuleEnv.varFrom(expr.idx), region); + } }, region); - return .{ - .stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }, - }; + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }; }, - .expect => |e| { - defer last_type_anno.* = null; // See above comment for why this is necessary - const region = self.parse_ir.tokenizedRegionToRegion(e.region); + .expect => |e_| { + const region = self.parse_ir.tokenizedRegionToRegion(e_.region); // Canonicalize the expect expression - const expr = try self.canonicalizeExprOrMalformed(e.body); + const expr = try self.canonicalizeExprOrMalformed(e_.body); // Create expect statement - const stmt_idx = try self.env.addStatementAndTypeVar(Statement{ .s_expect = .{ + const stmt_idx = try self.env.addStatement(Statement{ .s_expect = .{ .body = expr.idx, - } }, Content{ .structure = .empty_record }, region); + } }, region); - return .{ - .stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }, - }; + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }; }, .@"return" => |r| { - defer last_type_anno.* = null; // See above comment for why this is necessary + // To implement early returns and make them usable, we need to: + // 1. Update the parse to allow for if statements (as opposed to if expressions) + // 2. Track function scope in czer and capture the function for this return in `s_return` + // 3. When type checking a lambda, capture all early returns + // a. Unify all early returns together + // b. Unify early returns with func return type + const region = self.parse_ir.tokenizedRegionToRegion(r.region); // Canonicalize the return expression const expr = try self.canonicalizeExprOrMalformed(r.expr); - // Create return statement - const stmt_idx = try self.env.addStatementAndTypeVarRedirect(Statement{ .s_return = .{ + // Create return statement (lambda is null for now - will be implemented later) + const stmt_idx = try self.env.addStatement(Statement{ .s_return = .{ .expr = expr.idx, - } }, ModuleEnv.varFrom(expr.idx), region); + .lambda = null, + } }, region); - return .{ - .stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }, - }; + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }; }, - .type_decl => |s| { - defer last_type_anno.* = null; // See above comment for why this is necessary + .type_decl => |type_decl| { + // Type declarations in statement context (inside blocks/functions) + // These introduce local type aliases/nominals scoped to the current block + const region = self.parse_ir.tokenizedRegionToRegion(type_decl.region); - // TODO type declarations in statement context - const feature = try self.env.insertString("type_decl in statement context"); - const malformed_idx = try self.env.pushMalformed(Statement.Idx, Diagnostic{ .not_implemented = .{ - .feature = feature, - .region = self.parse_ir.tokenizedRegionToRegion(s.region), - } }); - return .{ - .stmt = CanonicalizedStatement{ .idx = malformed_idx, .free_vars = null }, + // Check if this is a type variable alias (e.g., `Thing : thing` where `thing` is a type var in scope) + // This enables static dispatch on type variables: `Thing.method(arg)` + const is_type_var_alias = type_var_alias_check: { + // Must be an alias (not nominal or opaque) + if (type_decl.kind != .alias) break :type_var_alias_check false; + + // Get the type header to check for type parameters + const ast_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch break :type_var_alias_check false; + + // Must have no type parameters (simple alias like `Thing : thing`, not `Thing(a) : thing`) + const header_args = self.parse_ir.store.typeAnnoSlice(ast_header.args); + if (header_args.len > 0) break :type_var_alias_check false; + + // Check if the annotation is a simple type variable + const ast_anno = self.parse_ir.store.getTypeAnno(type_decl.anno); + if (ast_anno != .ty_var) break :type_var_alias_check false; + + // Get the type variable name and check if it's in scope + const type_var_tok = ast_anno.ty_var.tok; + const type_var_ident = self.parse_ir.tokens.resolveIdentifier(type_var_tok) orelse break :type_var_alias_check false; + + // Check if this type variable is already in scope (from enclosing function signature) + const lookup_result = self.scopeLookupTypeVar(type_var_ident); + if (lookup_result != .found) break :type_var_alias_check false; + + break :type_var_alias_check true; }; - }, - .type_anno => |ta| { - // Note that we do _not_ defer resetting last_type_anno in this branch + if (is_type_var_alias) { + // This is a type variable alias - create s_type_var_alias statement + const ast_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch unreachable; + const alias_name = self.parse_ir.tokens.resolveIdentifier(ast_header.name) orelse unreachable; + + const ast_anno = self.parse_ir.store.getTypeAnno(type_decl.anno); + const type_var_tok = ast_anno.ty_var.tok; + const type_var_ident = self.parse_ir.tokens.resolveIdentifier(type_var_tok) orelse unreachable; + + // Get the type annotation index for the type variable from scope + const type_var_anno = switch (self.scopeLookupTypeVar(type_var_ident)) { + .found => |anno_idx| anno_idx, + .not_found => unreachable, // Already checked above + }; + + // Create the type var alias statement + const stmt_idx = try self.env.addStatement(Statement{ .s_type_var_alias = .{ + .alias_name = alias_name, + .type_var_name = type_var_ident, + .type_var_anno = type_var_anno, + } }, region); + + // Introduce the type var alias into scope for use in `Thing.method()` calls + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + _ = try current_scope.introduceTypeVarAlias(self.env.gpa, alias_name, type_var_ident, type_var_anno, stmt_idx, null); + + // Where clauses are not allowed + if (type_decl.where) |_| { + try self.env.pushDiagnostic(Diagnostic{ .where_clause_not_allowed_in_type_decl = .{ + .region = region, + } }); + } + + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = DataSpan.empty() }; + } else { + // Regular type alias or nominal type declaration + + // Canonicalize the type declaration header + const header_idx = try self.canonicalizeTypeHeader(type_decl.header, type_decl.kind); + + // Check if the header is malformed + const header_node = self.env.store.nodes.get(@enumFromInt(@intFromEnum(header_idx))); + if (header_node.tag == .malformed) { + // Header is malformed - return a malformed statement + const malformed_idx = try self.env.pushMalformed(Statement.Idx, Diagnostic{ .malformed_type_annotation = .{ + .region = region, + } }); + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; + } else { + // Get the type name from the header + const type_header = self.env.store.getTypeHeader(header_idx); + + // Process type parameters and annotation in a type variable scope + const anno_idx = blk: { + const type_var_scope = self.scopeEnterTypeVar(); + defer self.scopeExitTypeVar(type_var_scope); + + // Introduce type parameters from the header into the scope + try self.introduceTypeParametersFromHeader(header_idx); + + // Canonicalize the type annotation with type parameters in scope + break :blk try self.canonicalizeTypeAnno(type_decl.anno, .type_decl_anno); + }; + + // Create the CIR type declaration statement + const type_decl_stmt: Statement = switch (type_decl.kind) { + .alias => .{ + .s_alias_decl = .{ + .header = header_idx, + .anno = anno_idx, + }, + }, + .nominal, .@"opaque" => .{ + .s_nominal_decl = .{ + .header = header_idx, + .anno = anno_idx, + .is_opaque = type_decl.kind == .@"opaque", + }, + }, + }; + + const stmt_idx = try self.env.addStatement(type_decl_stmt, region); + + // Introduce the type into the current scope for local use + try self.introduceType(type_header.name, stmt_idx, region); + + // Where clauses are not allowed in type declarations + if (type_decl.where) |_| { + try self.env.pushDiagnostic(Diagnostic{ .where_clause_not_allowed_in_type_decl = .{ + .region = region, + } }); + } + + // Associated blocks are not supported for local type declarations + if (type_decl.associated) |_| { + try self.env.pushDiagnostic(Diagnostic{ .not_implemented = .{ + .feature = try self.env.insertString("associated blocks in local type declarations"), + .region = region, + } }); + } + + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = DataSpan.empty() }; + } + } + }, + .type_anno => |ta| blk: { // Type annotation statement const region = self.parse_ir.tokenizedRegionToRegion(ta.region); @@ -6037,19 +9234,25 @@ pub fn canonicalizeStatement( .feature = feature, .region = region, } }); - return .{ - .stmt = CanonicalizedStatement{ .idx = malformed_idx, .free_vars = null }, - }; + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = malformed_idx, .free_vars = DataSpan.empty() }; + break :blk; }; // Introduce type variables into scope const type_vars_top: u32 = @intCast(self.scratch_idents.top()); - // Extract type variables from the AST annotation - try self.extractTypeVarIdentsFromASTAnno(ta.anno, type_vars_top); + + // Create new type var scope + const type_var_scope = self.scopeEnterTypeVar(); + defer self.scopeExitTypeVar(type_var_scope); + // Now canonicalize the annotation with type variables in scope const type_anno_idx = try self.canonicalizeTypeAnno(ta.anno, .inline_anno); + + // Extract type variables from the AST annotation + try self.extractTypeVarIdentsFromASTAnno(ta.anno, type_vars_top); + // Canonicalize where clauses if present - const where_clauses = if (ta.where) |where_coll| blk: { + const where_clauses = if (ta.where) |where_coll| inner_blk: { const where_slice = self.parse_ir.store.whereClauseSlice(.{ .span = self.parse_ir.store.getCollection(where_coll).span }); const where_start = self.env.store.scratchWhereClauseTop(); @@ -6061,50 +9264,522 @@ pub fn canonicalizeStatement( const canonicalized_where = try self.canonicalizeWhereClause(where_idx, .inline_anno); try self.env.store.addScratchWhereClause(canonicalized_where); } - break :blk try self.env.store.whereClauseSpanFrom(where_start); + break :inner_blk try self.env.store.whereClauseSpanFrom(where_start); } else null; - // Create a type annotation statement - const type_anno_stmt: std.meta.FieldType(Statement, .s_type_anno) = .{ - .name = name_ident, - .anno = type_anno_idx, - .where = where_clauses, - }; - const type_anno_stmt_idx = try self.env.addStatementAndTypeVarRedirect(Statement{ - .s_type_anno = type_anno_stmt, - }, ModuleEnv.varFrom(type_anno_idx), region); + // Now, check the next stmt to see if it matches this anno + const next_i = current_index + 1; + if (next_i < ast_stmt_idxs.len) { + const next_stmt_id = ast_stmt_idxs[next_i]; + const next_stmt = self.parse_ir.store.getStatement(next_stmt_id); - last_type_anno.* = StmtTypeAnno{ - .anno_idx = type_anno_stmt_idx, - .anno = type_anno_stmt, - }; + switch (next_stmt) { + .decl => |decl| { + // Check if the decl name matches the anno name + const decl_pattern = self.parse_ir.store.getPattern(decl.pattern); + const names_match = name_check: { + if (decl_pattern == .ident) { + if (self.parse_ir.tokens.resolveIdentifier(decl_pattern.ident.ident_tok)) |decl_ident| { + break :name_check name_ident.idx == decl_ident.idx; + } + } + break :name_check false; + }; - return .{ - .stmt = CanonicalizedStatement{ .idx = type_anno_stmt_idx, .free_vars = null }, - }; + if (names_match) { + // Names match - immediately process the next decl with the annotation + mb_canonicailzed_stmt = try self.canonicalizeBlockDecl(decl, TypeAnnoIdent{ + .name = name_ident, + .anno_idx = type_anno_idx, + .where = where_clauses, + }); + stmts_processed = .two; + } else { + // Names don't match - create anno-only def for this anno + // and let the decl be processed separately in the next iteration + + // Check if a placeholder already exists (from Phase 1.5.5) + const pattern_idx = if (self.isPlaceholder(name_ident)) placeholder_check: { + // Reuse the existing placeholder pattern + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + const existing_pattern = current_scope.idents.get(name_ident) orelse { + // This shouldn't happen, but handle it gracefully + const pattern = Pattern{ + .assign = .{ + .ident = name_ident, + }, + }; + break :placeholder_check try self.env.addPattern(pattern, region); + }; + // Remove from placeholder tracking since we're making it real + _ = self.placeholder_idents.remove(name_ident); + break :placeholder_check existing_pattern; + } else create_new: { + // No placeholder - create new pattern and introduce to scope + const pattern = Pattern{ + .assign = .{ + .ident = name_ident, + }, + }; + const new_pattern_idx = try self.env.addPattern(pattern, region); + + // Introduce the name to scope + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, name_ident, new_pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = name_ident, + .region = region, + .original_region = original_region, + } }); + }, + else => {}, + } + break :create_new new_pattern_idx; + }; + + // Create the e_anno_only expression + const anno_only_expr = try self.env.addExpr(Expr{ .e_anno_only = .{} }, region); + + // Create the annotation structure + const annotation = CIR.Annotation{ + .anno = type_anno_idx, + .where = where_clauses, + }; + const annotation_idx = try self.env.addAnnotation(annotation, region); + + // Add the decl as a def so it gets included in all_defs + const def_idx = try self.env.addDef(.{ + .pattern = pattern_idx, + .expr = anno_only_expr, + .annotation = annotation_idx, + .kind = .let, + }, region); + try self.env.store.addScratchDef(def_idx); + + // Create the statement + const stmt_idx = try self.env.addStatement(Statement{ .s_decl = .{ + .pattern = pattern_idx, + .expr = anno_only_expr, + .anno = annotation_idx, + } }, region); + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = DataSpan.empty() }; + stmts_processed = .one; + } + }, + else => { + // If the next stmt does not match this annotation, + // create a Def with an e_anno_only body + + // Check if a placeholder already exists (from Phase 1.5.5) + const pattern_idx = if (self.isPlaceholder(name_ident)) placeholder_check2: { + // Reuse the existing placeholder pattern + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + const existing_pattern = current_scope.idents.get(name_ident) orelse { + const pattern = Pattern{ + .assign = .{ + .ident = name_ident, + }, + }; + break :placeholder_check2 try self.env.addPattern(pattern, region); + }; + _ = self.placeholder_idents.remove(name_ident); + break :placeholder_check2 existing_pattern; + } else create_new2: { + const pattern = Pattern{ + .assign = .{ + .ident = name_ident, + }, + }; + const new_pattern_idx = try self.env.addPattern(pattern, region); + + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, name_ident, new_pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = name_ident, + .region = region, + .original_region = original_region, + } }); + }, + else => {}, + } + break :create_new2 new_pattern_idx; + }; + + // Create the e_anno_only expression + const anno_only_expr = try self.env.addExpr(Expr{ .e_anno_only = .{} }, region); + + // Create the annotation structure + const annotation = CIR.Annotation{ + .anno = type_anno_idx, + .where = where_clauses, + }; + const annotation_idx = try self.env.addAnnotation(annotation, region); + + // Add the decl as a def so it gets included in all_defs + const def_idx = try self.env.addDef(.{ + .pattern = pattern_idx, + .expr = anno_only_expr, + .annotation = annotation_idx, + .kind = .let, + }, region); + try self.env.store.addScratchDef(def_idx); + + // Create the statement + const stmt_idx = try self.env.addStatement(Statement{ .s_decl = .{ + .pattern = pattern_idx, + .expr = anno_only_expr, + .anno = annotation_idx, + } }, region); + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = DataSpan.empty() }; + stmts_processed = .one; + }, + } + } else { + // If the next stmt does not match this annotation, + // create a Def with an e_anno_only body + + // Check if a placeholder already exists (from Phase 1.5.5) + const pattern_idx = if (self.isPlaceholder(name_ident)) placeholder_check3: { + // Reuse the existing placeholder pattern + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + const existing_pattern = current_scope.idents.get(name_ident) orelse { + const pattern = Pattern{ + .assign = .{ + .ident = name_ident, + }, + }; + break :placeholder_check3 try self.env.addPattern(pattern, region); + }; + _ = self.placeholder_idents.remove(name_ident); + break :placeholder_check3 existing_pattern; + } else create_new3: { + const pattern = Pattern{ + .assign = .{ + .ident = name_ident, + }, + }; + const new_pattern_idx = try self.env.addPattern(pattern, region); + + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, name_ident, new_pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = name_ident, + .region = region, + .original_region = original_region, + } }); + }, + else => {}, + } + break :create_new3 new_pattern_idx; + }; + + // Create the e_anno_only expression + const anno_only_expr = try self.env.addExpr(Expr{ .e_anno_only = .{} }, region); + + // Create the annotation structure + const annotation = CIR.Annotation{ + .anno = type_anno_idx, + .where = where_clauses, + }; + const annotation_idx = try self.env.addAnnotation(annotation, region); + + // Add the decl as a def so it gets included in all_defs + const def_idx = try self.env.addDef(.{ + .pattern = pattern_idx, + .expr = anno_only_expr, + .annotation = annotation_idx, + .kind = .let, + }, region); + try self.env.store.addScratchDef(def_idx); + + // Create the statement + const stmt_idx = try self.env.addStatement(Statement{ .s_decl = .{ + .pattern = pattern_idx, + .expr = anno_only_expr, + .anno = annotation_idx, + } }, region); + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = DataSpan.empty() }; + stmts_processed = .one; + } }, .import => |import_stmt| { - defer last_type_anno.* = null; // See above comment for why this is necessary - // After we process import statements, there's no need to include // then in the canonicalize IR _ = try self.canonicalizeImportStatement(import_stmt); - return .import_stmt; }, - else => { - defer last_type_anno.* = null; // See above comment for why this is necessary + .@"for" => |for_stmt| { + const region = self.parse_ir.tokenizedRegionToRegion(for_stmt.region); + const result = try self.canonicalizeForLoop(for_stmt.patt, for_stmt.expr, for_stmt.body); - // Other statement types not yet implemented - const feature = try self.env.insertString("statement type in block"); - const malformed_idx = try self.env.pushMalformed(Statement.Idx, Diagnostic{ .not_implemented = .{ - .feature = feature, - .region = Region.zero(), - } }); - return .{ - .stmt = CanonicalizedStatement{ .idx = malformed_idx, .free_vars = null }, + const stmt_idx = try self.env.addStatement(Statement{ + .s_for = .{ + .patt = result.patt, + .expr = result.list_expr, + .body = result.body, + }, + }, region); + + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = result.free_vars }; + }, + .@"while" => |while_stmt| { + // Use scratch_captures to collect free vars from both cond & body + const captures_top = self.scratch_captures.top(); + defer self.scratch_captures.clearFrom(captures_top); + + // Canonicalize the condition expression + // while $count < 10 { + // ^^^^^^^^^ + const cond = blk: { + const cond_free_vars_start = self.scratch_free_vars.top(); + defer self.scratch_free_vars.clearFrom(cond_free_vars_start); + + const czerd_cond = try self.canonicalizeExprOrMalformed(while_stmt.cond); + + // Copy free vars into captures (deduplicating) + const free_vars_slice = self.scratch_free_vars.sliceFromSpan(czerd_cond.free_vars); + for (free_vars_slice) |fv| { + if (!self.scratch_captures.contains(fv)) { + try self.scratch_captures.append(fv); + } + } + + break :blk czerd_cond; }; + + // Canonicalize the body + // while $count < 10 { + // print!($count.toStr()) <<<< + // $count = $count + 1 + // } + const body = blk: { + const body_free_vars_start = self.scratch_free_vars.top(); + defer self.scratch_free_vars.clearFrom(body_free_vars_start); + + const body_expr = try self.canonicalizeExprOrMalformed(while_stmt.body); + + // Copy free vars into captures (deduplicating) + const body_free_vars_slice = self.scratch_free_vars.sliceFromSpan(body_expr.free_vars); + for (body_free_vars_slice) |fv| { + if (!self.scratch_captures.contains(fv)) { + try self.scratch_captures.append(fv); + } + } + + break :blk body_expr; + }; + + // Copy captures to free_vars for parent + const free_vars_start = self.scratch_free_vars.top(); + const captures_slice = self.scratch_captures.sliceFromStart(captures_top); + for (captures_slice) |capture| { + try self.scratch_free_vars.append(capture); + } + const free_vars = self.scratch_free_vars.spanFrom(free_vars_start); + + // Insert into store + const region = self.parse_ir.tokenizedRegionToRegion(while_stmt.region); + const stmt_idx = try self.env.addStatement(Statement{ + .s_while = .{ + .cond = cond.idx, + .body = body.idx, + }, + }, region); + + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = free_vars }; + }, + .malformed => |_| { + // Stmt was malformed, parse reports this error, so do nothing here + mb_canonicailzed_stmt = null; }, } + + return StatementResult{ .canonicalized_stmt = mb_canonicailzed_stmt, .stmts_processed = stmts_processed }; +} + +/// Canonicalize a block declarataion +pub fn canonicalizeBlockDecl(self: *Self, d: AST.Statement.Decl, mb_last_anno: ?TypeAnnoIdent) std.mem.Allocator.Error!CanonicalizedStatement { + const region = self.parse_ir.tokenizedRegionToRegion(d.region); + + // Check if this is a var reassignment + const ast_pattern = self.parse_ir.store.getPattern(d.pattern); + switch (ast_pattern) { + .ident => |pattern_ident| { + const ident_region = self.parse_ir.tokenizedRegionToRegion(pattern_ident.region); + const ident_tok = pattern_ident.ident_tok; + + if (self.parse_ir.tokens.resolveIdentifier(ident_tok)) |ident_idx| { + // Check if this identifier exists and is a var + switch (self.scopeLookup(.ident, ident_idx)) { + .found => |existing_pattern_idx| { + // Check if this is a var reassignment across function boundaries + if (self.isVarReassignmentAcrossFunctionBoundary(existing_pattern_idx)) { + // Generate error for var reassignment across function boundary + const malformed_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .var_across_function_boundary = .{ + .region = ident_region, + } }); + + // Create a reassign statement with the error expression + const reassign_idx = try self.env.addStatement(Statement{ .s_reassign = .{ + .pattern_idx = existing_pattern_idx, + .expr = malformed_idx, + } }, ident_region); + + return CanonicalizedStatement{ .idx = reassign_idx, .free_vars = DataSpan.empty() }; + } + + // Check if this was declared as a var + if (self.isVarPattern(existing_pattern_idx)) { + // This is a var reassignment - canonicalize the expression and create reassign statement + const expr = try self.canonicalizeExprOrMalformed(d.body); + + // Create reassign statement + const reassign_idx = try self.env.addStatement(Statement{ .s_reassign = .{ + .pattern_idx = existing_pattern_idx, + .expr = expr.idx, + } }, ident_region); + + return CanonicalizedStatement{ .idx = reassign_idx, .free_vars = expr.free_vars }; + } + }, + .not_found => { + // Not found in scope, fall through to regular declaration + }, + } + } + }, + else => {}, + } + + // Check if this declaration matches the last type annotation + var mb_validated_anno: ?Annotation.Idx = null; + if (mb_last_anno) |anno_info| { + if (ast_pattern == .ident) { + const pattern_ident = ast_pattern.ident; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident.ident_tok)) |decl_ident| { + if (anno_info.name.idx == decl_ident.idx) { + // This declaration matches the type annotation + const pattern_region = self.parse_ir.tokenizedRegionToRegion(ast_pattern.to_tokenized_region()); + mb_validated_anno = try self.createAnnotationFromTypeAnno(anno_info.anno_idx, anno_info.where, pattern_region); + } + } + // Note: If resolveIdentifier returns null, the identifier token is malformed. + // The parser already handles this; we just don't match it with the annotation. + } + } + + // Regular declaration - canonicalize as usual + const pattern_idx = try self.canonicalizePattern(d.pattern) orelse inner_blk: { + const pattern = self.parse_ir.store.getPattern(d.pattern); + break :inner_blk try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = self.parse_ir.tokenizedRegionToRegion(pattern.to_tokenized_region()), + } }); + }; + + // Canonicalize the decl expr + const expr = try self.canonicalizeExprOrMalformed(d.body); + + // Determine if we should generalize based on RHS + const should_generalize = self.shouldGeneralizeBinding(expr.idx); + + // Create a declaration statement (generalized or not) + const stmt_idx = if (should_generalize) + try self.env.addStatement(Statement{ .s_decl_gen = .{ + .pattern = pattern_idx, + .expr = expr.idx, + .anno = mb_validated_anno, + } }, region) + else + try self.env.addStatement(Statement{ .s_decl = .{ + .pattern = pattern_idx, + .expr = expr.idx, + .anno = mb_validated_anno, + } }, region); + + return CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }; +} + +/// Determines whether a let binding should be generalized based on its RHS expression. +/// According to Roc's value restriction, only lambdas and number literals should be generalized. +fn shouldGeneralizeBinding(self: *Self, expr_idx: Expr.Idx) bool { + const expr = self.env.store.getExpr(expr_idx); + return switch (expr) { + // Lambdas should be generalized (both closures and pure lambdas) + .e_closure, .e_lambda => true, + + // Number literals should be generalized + .e_num, .e_frac_f32, .e_frac_f64, .e_dec, .e_dec_small => true, + + // Everything else should NOT be generalized + else => false, + }; +} + +// A canonicalized statement +const CanonicalizedStatement = struct { + idx: Statement.Idx, + free_vars: DataSpan, // This is a span into scratch_free_vars +}; + +// special type var scope // + +/// A type variable in scope +const TypeVarScope = struct { + ident: Ident.Idx, + anno_idx: CIR.TypeAnno.Idx, +}; + +/// Marker into the type var scope array, provided on scope enter, used on scope exit +const TypeVarScopeIdx = struct { idx: u32 }; + +/// Enter a type var scope +fn scopeEnterTypeVar(self: *Self) TypeVarScopeIdx { + return .{ .idx = self.type_vars_scope.top() }; +} + +/// Exit a type var scope +fn scopeExitTypeVar(self: *Self, scope_idx: TypeVarScopeIdx) void { + self.type_vars_scope.clearFrom(scope_idx.idx); +} + +/// Result of looking up a type variable +const TypeVarLookupResult = union(enum) { + found: CIR.TypeAnno.Idx, + not_found, +}; + +/// Lookup a type variable in the scope hierarchy +fn scopeLookupTypeVar(self: *const Self, name_ident: Ident.Idx) TypeVarLookupResult { + for (self.type_vars_scope.items.items) |entry| { + if (entry.ident.idx == name_ident.idx) { + return TypeVarLookupResult{ .found = entry.anno_idx }; + } + } + return .not_found; +} + +/// Result of introducing a type variable +const TypeVarIntroduceResult = union(enum) { + success, + already_in_scope: CIR.TypeAnno.Idx, +}; + +/// Introduce a type variable into the current scope +fn scopeIntroduceTypeVar(self: *Self, name_ident: Ident.Idx, type_var_anno: TypeAnno.Idx) std.mem.Allocator.Error!TypeVarIntroduceResult { + // Check if it's already in scope + for (self.type_vars_scope.items.items) |entry| { + if (entry.ident.idx == name_ident.idx) { + return .{ .already_in_scope = entry.anno_idx }; + } + } + + try self.type_vars_scope.append(TypeVarScope{ .ident = name_ident, .anno_idx = type_var_anno }); + return .success; } // scope // @@ -6112,50 +9787,73 @@ pub fn canonicalizeStatement( /// Enter a new scope level pub fn scopeEnter(self: *Self, gpa: std.mem.Allocator, is_function_boundary: bool) std.mem.Allocator.Error!void { const scope = Scope.init(is_function_boundary); - try self.scopes.append(gpa, scope); + return try self.scopeAppend(gpa, scope); } /// Exit the current scope level pub fn scopeExit(self: *Self, gpa: std.mem.Allocator) Scope.Error!void { + var popped_scope = try self.scopePop(); + popped_scope.deinit(gpa); +} + +/// Append an existing scope +pub fn scopeAppend(self: *Self, gpa: std.mem.Allocator, scope: Scope) std.mem.Allocator.Error!void { + try self.scopes.append(gpa, scope); +} + +/// Pop scope off the stack. +/// IMPORTANT: Caller owns the returned scope. +/// That is, this function does _not_ deinit the popped scope. +pub fn scopePop(self: *Self) Scope.Error!Scope { if (self.scopes.items.len <= 1) { return Scope.Error.ExitedTopScopeLevel; } - // Check for unused variables in the scope we're about to exit + // Check for undefined forward references in the scope we're about to exit const scope = &self.scopes.items[self.scopes.items.len - 1]; + var forward_ref_iter = scope.forward_references.iterator(); + while (forward_ref_iter.next()) |entry| { + const ident_idx = entry.key_ptr.*; + const forward_ref = entry.value_ptr.*; + + // This forward reference was never defined - report error for all reference sites + for (forward_ref.reference_regions.items) |ref_region| { + try self.env.pushDiagnostic(Diagnostic{ .ident_not_in_scope = .{ + .ident = ident_idx, + .region = ref_region, + } }); + } + } + + // Check for unused variables in the scope we're about to exit try self.checkScopeForUnusedVariables(scope); - var popped_scope: Scope = self.scopes.pop().?; - popped_scope.deinit(gpa); + const popped_scope: Scope = self.scopes.pop().?; + return popped_scope; } /// Get the current scope -fn currentScope(self: *Self) *Scope { +pub fn currentScope(self: *Self) *Scope { std.debug.assert(self.scopes.items.len > 0); - return &self.scopes.items[self.scopes.items.len - 1]; + return &self.scopes.items[self.currentScopeIdx()]; +} + +/// Get the current scope +fn currentScopeIdx(self: *Self) usize { + std.debug.assert(self.scopes.items.len > 0); + return self.scopes.items.len - 1; } /// This will be used later for builtins like Num.nan, Num.infinity, etc. pub fn addNonFiniteFloat(self: *Self, value: f64, region: base.Region) !Expr.Idx { - // Dec doesn't have infinity, -infinity, or NaN - const requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = true, - .fits_in_dec = false, - }; - - const frac_requirements = types.Num.FracRequirements{ - .fits_in_f32 = requirements.fits_in_f32, - .fits_in_dec = requirements.fits_in_dec, - }; - // then in the final slot the actual expr is inserted - const expr_idx = try self.env.addExprAndTypeVar( + const expr_idx = try self.env.addExpr( CIR.Expr{ .e_frac_f64 = .{ .value = value, + .has_suffix = false, }, }, - Content{ .structure = .{ .num = .{ .frac_unbound = frac_requirements } } }, region, ); @@ -6174,11 +9872,8 @@ fn scopeContains( const scope = &self.scopes.items[scope_idx]; const map = scope.itemsConst(item_kind); - var iter = map.iterator(); - while (iter.next()) |entry| { - if (name.idx == entry.key_ptr.idx) { - return entry.value_ptr.*; - } + if (map.get(name)) |pattern_idx| { + return pattern_idx; } } return null; @@ -6190,62 +9885,20 @@ pub fn scopeLookup( comptime item_kind: Scope.ItemKind, name: base.Ident.Idx, ) Scope.LookupResult { - if (self.scopeContains(item_kind, name)) |pattern| { - return Scope.LookupResult{ .found = pattern }; + if (self.scopeContains(item_kind, name)) |found| { + return Scope.LookupResult{ .found = found }; } return Scope.LookupResult{ .not_found = {} }; } -/// Lookup a type variable in the scope hierarchy -fn scopeLookupTypeVar(self: *const Self, name_ident: Ident.Idx) ?TypeAnno.Idx { - // Search from innermost to outermost scope - var i = self.scopes.items.len; - while (i > 0) { - i -= 1; - const scope = &self.scopes.items[i]; - - switch (scope.lookupTypeVar(name_ident)) { - .found => |type_var_idx| return type_var_idx, - .not_found => continue, - } - } - return null; -} - -/// Introduce a type variable into the current scope -fn scopeIntroduceTypeVar(self: *Self, name: Ident.Idx, type_var_anno: TypeAnno.Idx) std.mem.Allocator.Error!void { - const gpa = self.env.gpa; - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - - // Don't use parent lookup function for now - just introduce directly - // Type variable shadowing is allowed in Roc - const result = try current_scope.introduceTypeVar(gpa, name, type_var_anno, null); - - switch (result) { - .success => {}, - .shadowing_warning => |shadowed_type_var_idx| { - // Type variable shadowing is allowed but should produce warning - const original_region = self.env.store.getTypeAnnoRegion(shadowed_type_var_idx); - try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ - .ident = name, - .region = self.env.store.getTypeAnnoRegion(type_var_anno), - .original_region = original_region, - } }); - }, - .already_in_scope => |_| { - // Type variable already exists in this scope - this is fine for repeated references - }, - } -} - fn introduceTypeParametersFromHeader(self: *Self, header_idx: CIR.TypeHeader.Idx) std.mem.Allocator.Error!void { const header = self.env.store.getTypeHeader(header_idx); // Introduce each type parameter into the current scope for (self.env.store.sliceTypeAnnos(header.args)) |param_idx| { const param = self.env.store.getTypeAnno(param_idx); - if (param == .ty_var) { - try self.scopeIntroduceTypeVar(param.ty_var.name, param_idx); + if (param == .rigid_var) { + _ = try self.scopeIntroduceTypeVar(param.rigid_var.name, param_idx); } } } @@ -6259,7 +9912,7 @@ fn extractTypeVarIdentsFromASTAnno(self: *Self, anno_idx: AST.TypeAnno.Idx, iden for (self.scratch_idents.sliceFromStart(idents_start_idx)) |existing| { if (existing.idx == ident.idx) return; // Already added } - _ = try self.scratch_idents.append(self.env.gpa, ident); + try self.scratch_idents.append(ident); } }, .underscore_type_var => |underscore_ty_var| { @@ -6268,7 +9921,7 @@ fn extractTypeVarIdentsFromASTAnno(self: *Self, anno_idx: AST.TypeAnno.Idx, iden for (self.scratch_idents.sliceFromStart(idents_start_idx)) |existing| { if (existing.idx == ident.idx) return; // Already added } - try self.scratch_idents.append(self.env.gpa, ident); + try self.scratch_idents.append(ident); } }, .apply => |apply| { @@ -6299,7 +9952,17 @@ fn extractTypeVarIdentsFromASTAnno(self: *Self, anno_idx: AST.TypeAnno.Idx, iden try self.extractTypeVarIdentsFromASTAnno(field.ty, idents_start_idx); } }, - .ty, .underscore, .tag_union, .malformed => { + .tag_union => |tag_union| { + // Extract type variables from tags + for (self.parse_ir.store.typeAnnoSlice(tag_union.tags)) |tag_idx| { + try self.extractTypeVarIdentsFromASTAnno(tag_idx, idents_start_idx); + } + // Extract type variable from open extension if present + if (tag_union.open_anno) |open_idx| { + try self.extractTypeVarIdentsFromASTAnno(open_idx, idents_start_idx); + } + }, + .ty, .underscore, .malformed => { // These don't contain type variables to extract }, } @@ -6397,8 +10060,27 @@ pub fn scopeIntroduceInternal( return Scope.IntroduceResult{ .top_level_var_error = {} }; } + // Check if this identifier was previously referenced as a forward reference + // If so, upgrade it from forward reference to defined + if (item_kind == .ident) { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + if (current_scope.forward_references.fetchRemove(ident_idx)) |kv| { + // This was a forward reference - upgrade it to defined + // The pattern is already in the idents map from when we created the forward ref + // Just update it to point to the real pattern + try current_scope.idents.put(gpa, ident_idx, pattern_idx); + + // Clean up the reference regions arraylist + var mut_regions = kv.value.reference_regions; + mut_regions.deinit(gpa); + + // Return success - forward reference successfully upgraded + return Scope.IntroduceResult{ .success = {} }; + } + } + // Check for existing identifier in any scope level for shadowing detection - if (self.scopeContains(item_kind, ident_idx)) |existing_pattern| { + if (self.scopeContains(item_kind, ident_idx)) |existing| { // If it's a var reassignment (not declaration), check function boundaries if (is_var and !is_declaration) { // Find the scope where the var was declared and check for function boundaries @@ -6411,15 +10093,10 @@ pub fn scopeIntroduceInternal( const scope = &self.scopes.items[scope_idx]; const map = scope.itemsConst(item_kind); - var iter = map.iterator(); - while (iter.next()) |entry| { - if (ident_idx.idx == entry.key_ptr.idx) { - declaration_scope_idx = scope_idx; - break; - } + if (map.get(ident_idx) != null) { + declaration_scope_idx = scope_idx; + break; } - - if (declaration_scope_idx != null) break; } // Now check if there are function boundaries between declaration and current scope @@ -6438,7 +10115,7 @@ pub fn scopeIntroduceInternal( if (found_function_boundary) { // Different function, return error - return Scope.IntroduceResult{ .var_across_function_boundary = existing_pattern }; + return Scope.IntroduceResult{ .var_across_function_boundary = existing }; } else { // Same function, allow reassignment without warning try self.scopes.items[self.scopes.items.len - 1].put(gpa, item_kind, ident_idx, pattern_idx); @@ -6454,7 +10131,7 @@ pub fn scopeIntroduceInternal( // For non-var declarations, we should still report shadowing // Regular shadowing case - produce warning but still introduce try self.scopes.items[self.scopes.items.len - 1].put(gpa, item_kind, ident_idx, pattern_idx); - return Scope.IntroduceResult{ .shadowing_warning = existing_pattern }; + return Scope.IntroduceResult{ .shadowing_warning = existing }; } // Check the current level for duplicates @@ -6475,6 +10152,27 @@ pub fn scopeIntroduceInternal( return Scope.IntroduceResult{ .success = {} }; } +/// Introduce a value identifier to scope and report shadowing diagnostics if needed +fn introduceValue( + self: *Self, + ident_idx: base.Ident.Idx, + pattern_idx: Pattern.Idx, + region: Region, +) std.mem.Allocator.Error!void { + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident_idx, pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = ident_idx, + .region = region, + .original_region = original_region, + } }); + }, + .top_level_var_error, .var_across_function_boundary => {}, + } +} + /// Check if an identifier is marked as ignored (underscore prefix) fn identIsIgnored(ident_idx: base.Ident.Idx) bool { return ident_idx.attributes.ignored; @@ -6502,8 +10200,8 @@ fn checkScopeForUnusedVariables(self: *Self, scope: *const Scope) std.mem.Alloca const UnusedVar = struct { ident: base.Ident.Idx, region: Region }; // Collect all unused variables first so we can sort them - var unused_vars = std.ArrayList(UnusedVar).init(self.env.gpa); - defer unused_vars.deinit(); + var unused_vars = std.ArrayList(UnusedVar).empty; + defer unused_vars.deinit(self.env.gpa); // Iterate through all identifiers in this scope var iterator = scope.idents.iterator(); @@ -6521,11 +10219,34 @@ fn checkScopeForUnusedVariables(self: *Self, scope: *const Scope) std.mem.Alloca continue; } + // Skip if this identifier is exposed (implicitly used in type modules) + if (self.env.common.exposed_items.containsById(self.env.gpa, @bitCast(ident_idx))) { + continue; + } + + // Get the pattern to check if it has a different ident than the scope key + // For pattern_identifier nodes, check if the qualified ident is exposed + const node_idx: Node.Idx = @enumFromInt(@intFromEnum(pattern_idx)); + + // Skip if the pattern doesn't have a corresponding node (e.g., in tests with fake indices) + if (@intFromEnum(node_idx) >= self.env.store.nodes.len()) { + continue; + } + + const node = self.env.store.nodes.get(node_idx); + + if (node.tag == .pattern_identifier) { + const assign_ident: base.Ident.Idx = @bitCast(node.data_1); + if (self.env.common.exposed_items.containsById(self.env.gpa, @bitCast(assign_ident))) { + continue; + } + } + // Get the region for this pattern to provide good error location const region = self.env.store.getPatternRegion(pattern_idx); // Collect unused variable for sorting - try unused_vars.append(.{ + try unused_vars.append(self.env.gpa, .{ .ident = ident_idx, .region = region, }); @@ -6549,15 +10270,35 @@ fn checkScopeForUnusedVariables(self: *Self, scope: *const Scope) std.mem.Alloca } /// Introduce a type declaration into the current scope -fn scopeIntroduceTypeDecl( +pub fn introduceType( self: *Self, name_ident: Ident.Idx, type_decl_stmt: Statement.Idx, region: Region, ) std.mem.Allocator.Error!void { const gpa = self.env.gpa; + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + // Check if trying to redeclare an auto-imported builtin type + if (self.module_envs) |envs_map| { + // Check if this name matches an auto-imported module + if (envs_map.get(name_ident)) |_| { + // This is an auto-imported builtin type - report error + // Use Region.zero() since auto-imported types don't have a meaningful source location + const original_region = Region.zero(); + + try self.env.pushDiagnostic(Diagnostic{ + .type_redeclared = .{ + .original_region = original_region, + .redeclared_region = region, + .name = name_ident, + }, + }); + return; + } + } + // Check for shadowing in parent scopes var shadowed_in_parent: ?Statement.Idx = null; if (self.scopes.items.len > 1) { @@ -6565,12 +10306,14 @@ fn scopeIntroduceTypeDecl( while (i > 0) { i -= 1; const scope = &self.scopes.items[i]; - switch (scope.lookupTypeDecl(name_ident)) { - .found => |type_decl_idx| { - shadowed_in_parent = type_decl_idx; - break; - }, - .not_found => continue, + if (scope.type_bindings.get(name_ident)) |binding| { + shadowed_in_parent = switch (binding) { + .local_nominal => |stmt| stmt, + .local_alias => |stmt| stmt, + .associated_nominal => |stmt| stmt, + .external_nominal => null, + }; + if (shadowed_in_parent) |_| break; } } } @@ -6659,6 +10402,32 @@ fn scopeIntroduceTypeDecl( } } +/// Check if an identifier is a placeholder, with fast path for empty map (99% of files). +/// Returns true if the identifier is tracked as a placeholder. +fn isPlaceholder(self: *const Self, ident_idx: Ident.Idx) bool { + // Fast path: if map is empty, no placeholders exist + if (self.placeholder_idents.count() == 0) return false; + return self.placeholder_idents.contains(ident_idx); +} + +/// Update a placeholder pattern in scope with the actual pattern. +/// In debug builds, asserts that the identifier was tracked as a placeholder. +fn updatePlaceholder( + self: *Self, + scope: *Scope, + ident_idx: Ident.Idx, + pattern_idx: Pattern.Idx, +) std.mem.Allocator.Error!void { + if (builtin.mode == .Debug) { + std.debug.assert(self.isPlaceholder(ident_idx)); + } + // Remove from placeholder tracking since it's now a real definition + if (self.placeholder_idents.count() > 0) { + _ = self.placeholder_idents.remove(ident_idx); + } + try scope.idents.put(self.env.gpa, ident_idx, pattern_idx); +} + fn scopeUpdateTypeDecl( self: *Self, name_ident: Ident.Idx, @@ -6669,16 +10438,48 @@ fn scopeUpdateTypeDecl( try current_scope.updateTypeDecl(gpa, name_ident, new_type_decl_stmt); } -fn scopeLookupTypeDecl(self: *Self, ident_idx: Ident.Idx) ?Statement.Idx { +/// Look up a type declaration by identifier, searching from innermost to outermost scope. +pub fn scopeLookupTypeDecl(self: *Self, ident_idx: Ident.Idx) ?Statement.Idx { // Search from innermost to outermost scope var i = self.scopes.items.len; while (i > 0) { i -= 1; const scope = &self.scopes.items[i]; - switch (scope.lookupTypeDecl(ident_idx)) { - .found => |type_decl_idx| return type_decl_idx, - .not_found => continue, + // Check unified type bindings + if (scope.type_bindings.get(ident_idx)) |binding| { + return switch (binding) { + .local_nominal => |stmt| stmt, + .local_alias => |stmt| stmt, + .associated_nominal => |stmt| stmt, + .external_nominal => null, // External types don't have local Statement.Idx + }; + } + } + + return null; +} + +fn scopeLookupTypeBinding(self: *Self, ident_idx: Ident.Idx) ?TypeBindingLocation { + var i = self.scopes.items.len; + while (i > 0) { + i -= 1; + const scope = &self.scopes.items[i]; + if (scope.type_bindings.getPtr(ident_idx)) |binding_ptr| { + return TypeBindingLocation{ .scope_index = i, .binding = binding_ptr }; + } + } + + return null; +} + +fn scopeLookupTypeBindingConst(self: *const Self, ident_idx: Ident.Idx) ?TypeBindingLocationConst { + var i = self.scopes.items.len; + while (i > 0) { + i -= 1; + const scope = &self.scopes.items[i]; + if (scope.type_bindings.getPtr(ident_idx)) |binding_ptr| { + return TypeBindingLocationConst{ .scope_index = i, .binding = binding_ptr }; } } @@ -6686,7 +10487,7 @@ fn scopeLookupTypeDecl(self: *Self, ident_idx: Ident.Idx) ?Statement.Idx { } /// Look up a module alias in the scope hierarchy -fn scopeLookupModule(self: *const Self, alias_name: Ident.Idx) ?Ident.Idx { +fn scopeLookupModule(self: *const Self, alias_name: Ident.Idx) ?Scope.ModuleAliasInfo { // Search from innermost to outermost scope var i = self.scopes.items.len; while (i > 0) { @@ -6694,7 +10495,7 @@ fn scopeLookupModule(self: *const Self, alias_name: Ident.Idx) ?Ident.Idx { const scope = &self.scopes.items[i]; switch (scope.lookupModuleAlias(alias_name)) { - .found => |module_name| return module_name, + .found => |module_info| return module_info, .not_found => continue, } } @@ -6703,43 +10504,76 @@ fn scopeLookupModule(self: *const Self, alias_name: Ident.Idx) ?Ident.Idx { } /// Introduce a module alias into scope -fn scopeIntroduceModuleAlias(self: *Self, alias_name: Ident.Idx, module_name: Ident.Idx) std.mem.Allocator.Error!void { +fn scopeIntroduceModuleAlias(self: *Self, alias_name: Ident.Idx, module_name: Ident.Idx, import_region: Region, exposed_items_span: CIR.ExposedItem.Span, is_package_qualified: bool) std.mem.Allocator.Error!void { const gpa = self.env.gpa; + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + // Check if this alias conflicts with an existing type binding (e.g., auto-imported type or primitive builtin) + // Primitive builtins (Str, List, Box) are now added to type_bindings in setupAutoImportedBuiltinTypes + if (current_scope.type_bindings.get(alias_name)) |existing_binding| { + // Check if any exposed items have the same name as the alias + // If so, skip the error here and let introduceItemsAliased handle it + const exposed_items_slice = self.env.store.sliceExposedItems(exposed_items_span); + for (exposed_items_slice) |exposed_item_idx| { + const exposed_item = self.env.store.getExposedItem(exposed_item_idx); + const local_ident = exposed_item.alias orelse exposed_item.name; + + if (local_ident.idx == alias_name.idx) { + // The alias has the same name as an exposed item, so skip reporting + // the error here - it will be reported by introduceItemsAliased + return; + } + } + + // Get the original region from the existing binding + const original_region = switch (existing_binding) { + .external_nominal => |ext| ext.origin_region, + else => Region.zero(), + }; + + try self.env.pushDiagnostic(Diagnostic{ + .shadowing_warning = .{ + .ident = alias_name, + .region = import_region, + .original_region = original_region, + }, + }); + + // Don't add the duplicate binding + return; + } + // Simplified introduction without parent lookup for now - const result = try current_scope.introduceModuleAlias(gpa, alias_name, module_name, null); + const result = try current_scope.introduceModuleAlias(gpa, alias_name, module_name, is_package_qualified, null); switch (result) { .success => {}, - .shadowing_warning => |shadowed_module| { + .shadowing_warning => { // Create diagnostic for module alias shadowing try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ .ident = alias_name, - .region = Region.zero(), // TODO: get proper region - .original_region = Region.zero(), // TODO: get proper region + .region = import_region, + .original_region = Region.zero(), }, }); - _ = shadowed_module; // Suppress unused variable warning }, - .already_in_scope => |existing_module| { + .already_in_scope => { // Module alias already exists in current scope - // For now, just issue a diagnostic try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ .ident = alias_name, - .region = Region.zero(), // TODO: get proper region - .original_region = Region.zero(), // TODO: get proper region + .region = import_region, + .original_region = Region.zero(), }, }); - _ = existing_module; // Suppress unused variable warning }, } } /// Helper function to look up module aliases in parent scopes only -fn scopeLookupModuleInParentScopes(self: *const Self, alias_name: Ident.Idx) ?Ident.Idx { +fn scopeLookupModuleInParentScopes(self: *const Self, alias_name: Ident.Idx) ?Scope.ModuleAliasInfo { // Search from second-innermost to outermost scope (excluding current scope) if (self.scopes.items.len <= 1) return null; @@ -6748,8 +10582,8 @@ fn scopeLookupModuleInParentScopes(self: *const Self, alias_name: Ident.Idx) ?Id i -= 1; const scope = &self.scopes.items[i]; - switch (scope.lookupModuleAlias(&self.env.idents, alias_name)) { - .found => |module_name| return module_name, + switch (scope.lookupModuleAlias(alias_name)) { + .found => |module_info| return module_info, .not_found => continue, } } @@ -6775,8 +10609,9 @@ fn scopeLookupExposedItem(self: *const Self, item_name: Ident.Idx) ?Scope.Expose } /// Introduce an exposed item into the current scope -fn scopeIntroduceExposedItem(self: *Self, item_name: Ident.Idx, item_info: Scope.ExposedItemInfo) std.mem.Allocator.Error!void { +pub fn scopeIntroduceExposedItem(self: *Self, item_name: Ident.Idx, item_info: Scope.ExposedItemInfo, import_region: Region) std.mem.Allocator.Error!void { const gpa = self.env.gpa; + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; // Simplified introduction without parent lookup for now @@ -6798,7 +10633,7 @@ fn scopeIntroduceExposedItem(self: *Self, item_name: Ident.Idx, item_info: Scope try self.env.pushDiagnostic(Diagnostic{ .not_implemented = .{ .feature = message_str, - .region = Region.zero(), // TODO: Get proper region from import statement + .region = import_region, }, }); }, @@ -6815,13 +10650,104 @@ fn scopeIntroduceExposedItem(self: *Self, item_name: Ident.Idx, item_info: Scope try self.env.pushDiagnostic(Diagnostic{ .not_implemented = .{ .feature = message_str, - .region = Region.zero(), // TODO: Get proper region from import statement + .region = import_region, }, }); }, } } +/// Set an external type binding for an imported nominal type +/// Also adds the qualified type name to the import mapping for error message display. +fn setExternalTypeBinding( + self: *Self, + scope: *Scope, + local_ident: Ident.Idx, + module_ident: Ident.Idx, + original_ident: Ident.Idx, + original_type_name: []const u8, + target_node_idx: ?u16, + module_import_idx: CIR.Import.Idx, + origin_region: Region, + module_found_status: ModuleFoundStatus, +) !void { + // Check if type already exists in this scope (mirrors Scope.introduceTypeDecl logic) + if (scope.type_bindings.get(local_ident)) |existing_binding| { + // Extract the original region from the existing binding for the diagnostic + const original_region = switch (existing_binding) { + .local_nominal, .local_alias, .associated_nominal => Region.zero(), + .external_nominal => |ext| ext.origin_region, + }; + + // Report duplicate definition error + try self.env.pushDiagnostic(Diagnostic{ + .shadowing_warning = .{ + .ident = local_ident, + .region = origin_region, + .original_region = original_region, + }, + }); + + // Don't add the duplicate binding + return; + } + + try scope.type_bindings.put(self.env.gpa, local_ident, Scope.TypeBinding{ + .external_nominal = .{ + .module_ident = module_ident, + .original_ident = original_ident, + .target_node_idx = target_node_idx, + .import_idx = module_import_idx, + .origin_region = origin_region, + .module_not_found = module_found_status == .module_not_found, + }, + }); + + // Add to import mapping: qualified_name -> local_name + // This allows error messages to display the user's preferred name for the type + const module_name_text = self.env.getIdent(module_ident); + + // Build the fully-qualified type name (e.g., "MyModule.Foo") + const qualified_name = try std.fmt.allocPrint(self.env.gpa, "{s}.{s}", .{ module_name_text, original_type_name }); + defer self.env.gpa.free(qualified_name); + + // Intern the qualified name in the current module's ident store + const qualified_ident = try self.env.insertIdent(Ident.for_text(qualified_name)); + + // Add the mapping from qualified ident to local ident + // Only replace if the new name is "better" (shortest wins, lexicographic tiebreaker) + const local_name = self.env.getIdent(local_ident); + if (self.env.import_mapping.get(qualified_ident)) |existing_ident| { + const existing_name = self.env.getIdent(existing_ident); + if (displayNameIsBetter(local_name, existing_name)) { + try self.env.import_mapping.put(qualified_ident, local_ident); + } + } else { + try self.env.import_mapping.put(qualified_ident, local_ident); + } +} + +/// Determine if `new_name` is a "better" display name than `existing_name`. +/// Returns true if new_name should replace existing_name. +/// +/// The rules are: +/// 1. Shorter names are better (fewer characters to read in error messages) +/// 2. For equal lengths, lexicographically smaller wins (deterministic regardless of import order) +fn displayNameIsBetter(new_name: []const u8, existing_name: []const u8) bool { + // Shorter is better + if (new_name.len != existing_name.len) { + return new_name.len < existing_name.len; + } + // Equal length: lexicographic comparison (lower byte value wins) + for (new_name, existing_name) |new_byte, existing_byte| { + if (new_byte != existing_byte) { + return new_byte < existing_byte; + } + } + // Identical strings - no replacement needed + return false; +} + /// Look up an exposed item in parent scopes (for shadowing detection) fn scopeLookupExposedItemInParentScopes(self: *const Self, item_name: Ident.Idx) ?Scope.ExposedItemInfo { // Search from second-innermost to outermost scope (excluding current scope) @@ -6858,6 +10784,34 @@ fn scopeLookupImportedModule(self: *const Self, module_name: []const u8) ?Import return null; } +/// Get or create an import index for an auto-imported module like Bool or Try +fn getOrCreateAutoImport(self: *Self, module_name_text: []const u8) std.mem.Allocator.Error!Import.Idx { + // Check if we already have an import for this module + if (self.import_indices.get(module_name_text)) |existing_idx| { + return existing_idx; + } + + // Create ident for index-based lookups + const module_ident = try self.env.insertIdent(base.Ident.for_text(module_name_text)); + + // Create a new import using the imports map (with ident for index-based lookups) + const new_import_idx = try self.env.imports.getOrPutWithIdent( + self.env.gpa, + self.env.common.getStringStore(), + module_name_text, + module_ident, + ); + + // Store it in our import map + try self.import_indices.put(self.env.gpa, module_name_text, new_import_idx); + + // Also add to current scope so scopeLookupImportedModule can find it + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + _ = try current_scope.introduceImportedModule(self.env.gpa, module_name_text, new_import_idx); + + return new_import_idx; +} + /// Extract the module name from a full qualified name (e.g., "Json" from "json.Json") fn extractModuleName(self: *Self, module_name_ident: Ident.Idx) std.mem.Allocator.Error!Ident.Idx { const module_text = self.env.getIdent(module_name_ident); @@ -6883,21 +10837,66 @@ fn canonicalizeWhereClause(self: *Self, ast_where_idx: AST.WhereClause.Idx, type .mod_method => |mm| { const region = self.parse_ir.tokenizedRegionToRegion(mm.region); - // Resolve type variable name - const var_name = self.parse_ir.resolve(mm.var_tok); + // Get variable being referenced + // where [ a.method : ... ] + // ^ + const var_name_text = self.parse_ir.resolve(mm.var_tok); + const var_ident = try self.env.insertIdent(Ident.for_text(var_name_text)); - // Resolve method name (remove leading dot) - const method_name_text = self.parse_ir.resolve(mm.name_tok); + // Find the variable in scope + const var_anno_idx = + switch (self.scopeLookupTypeVar(var_ident)) { + .found => |found_anno_idx| blk: { + // Track this type variable for underscore validation + try self.scratch_type_var_validation.append(var_ident); - // Remove leading dot from method name - const method_name_clean = if (method_name_text.len > 0 and method_name_text[0] == '.') - method_name_text[1..] - else - method_name_text; + break :blk try self.env.addTypeAnno(.{ .rigid_var_lookup = .{ + .ref = found_anno_idx, + } }, region); + }, + .not_found => blk: { + switch (type_anno_ctx) { + // If this is an inline anno, then we can introduce the variable + // into the scope + .inline_anno => { + // Track this type variable for underscore validation + try self.scratch_type_var_validation.append(var_ident); - // Intern the variable and method names - const var_ident = try self.env.insertIdent(Ident.for_text(var_name)); - const method_ident = try self.env.insertIdent(Ident.for_text(method_name_clean)); + const new_anno_idx = try self.env.addTypeAnno(.{ .rigid_var = .{ + .name = var_ident, + } }, region); + + // Add to scope + _ = try self.scopeIntroduceTypeVar(var_ident, new_anno_idx); + + break :blk new_anno_idx; + }, + // Otherwise, this is malformed + .type_decl_anno => { + break :blk try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .undeclared_type_var = .{ + .name = var_ident, + .region = region, + } }); + }, + } + }, + }; + + // Get alias being referenced + // where [ a.method : ... ] + // ^^^^^^ + const method_ident = blk: { + // Resolve alias name (remove leading dot) + const method_name_text = self.parse_ir.resolve(mm.name_tok); + + // Remove leading dot from method name + const method_name_clean = if (method_name_text.len > 0 and method_name_text[0] == '.') + method_name_text[1..] + else + method_name_text; + + break :blk try self.env.insertIdent(Ident.for_text(method_name_clean)); + }; // Canonicalize argument types const args_slice = self.parse_ir.store.typeAnnoSlice(.{ .span = self.parse_ir.store.getCollection(mm.args).span }); @@ -6909,104 +10908,113 @@ fn canonicalizeWhereClause(self: *Self, ast_where_idx: AST.WhereClause.Idx, type const args_span = try self.env.store.typeAnnoSpanFrom(args_start); // Canonicalize return type - const ret_anno = try self.canonicalizeTypeAnno(mm.ret_anno, type_anno_ctx); + const ret = try self.canonicalizeTypeAnno(mm.ret_anno, type_anno_ctx); - // Create external declaration for where clause method constraint - // This represents the requirement that type variable must come from a module - // that provides the specified method - const var_name_text = self.env.getIdent(var_ident); - - // Create qualified name: "module(a).method" - const qualified_text = try std.fmt.allocPrint(self.env.gpa, "module({s}).{s}", .{ var_name_text, method_name_clean }); - defer self.env.gpa.free(qualified_text); - const qualified_name = try self.env.insertIdent(Ident.for_text(qualified_text)); - - // Create module name: "module(a)" - const module_text = try std.fmt.allocPrint(self.env.gpa, "module({s})", .{var_name_text}); - defer self.env.gpa.free(module_text); - const module_name = try self.env.insertIdent(Ident.for_text(module_text)); - - const external_type_var = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .{ .flex_var = null }, region, TypeVar); - const external_decl = try self.createExternalDeclaration(qualified_name, module_name, method_ident, .value, external_type_var, region); - - return try self.env.addWhereClauseAndTypeVar(WhereClause{ .mod_method = .{ - .var_name = var_ident, + return try self.env.addWhereClause(WhereClause{ .w_method = .{ + .var_ = var_anno_idx, .method_name = method_ident, .args = args_span, - .ret_anno = ret_anno, - .external_decl = external_decl, - } }, .{ .flex_var = null }, region); + .ret = ret, + } }, region); }, .mod_alias => |ma| { const region = self.parse_ir.tokenizedRegionToRegion(ma.region); - // Resolve type variable name - const var_name = self.parse_ir.resolve(ma.var_tok); + // Get variable being referenced + // where [ a.Alias ] + // ^ + const var_name_text = self.parse_ir.resolve(ma.var_tok); + const var_ident = try self.env.insertIdent(Ident.for_text(var_name_text)); - // Resolve alias name (remove leading dot) - const alias_name_text = self.parse_ir.resolve(ma.name_tok); + // Find the variable in scope + const var_anno_idx = + switch (self.scopeLookupTypeVar(var_ident)) { + .found => |found_anno_idx| blk: { + // Track this type variable for underscore validation + try self.scratch_type_var_validation.append(var_ident); - // Remove leading dot from alias name - const alias_name_clean = if (alias_name_text.len > 0 and alias_name_text[0] == '.') - alias_name_text[1..] - else - alias_name_text; + break :blk try self.env.addTypeAnno(.{ .rigid_var_lookup = .{ + .ref = found_anno_idx, + } }, region); + }, + .not_found => blk: { + switch (type_anno_ctx) { + // If this is an inline anno, then we can introduce the variable + // into the scope + .inline_anno => { + // Track this type variable for underscore validation + try self.scratch_type_var_validation.append(var_ident); - // Intern the variable and alias names - const var_ident = try self.env.insertIdent(Ident.for_text(var_name)); - const alias_ident = try self.env.insertIdent(Ident.for_text(alias_name_clean)); + const new_anno_idx = try self.env.addTypeAnno(.{ .rigid_var = .{ + .name = var_ident, + } }, region); - // Create external declaration for where clause alias constraint - // This represents the requirement that type variable must come from a module - // that provides the specified type alias - const var_name_text = self.env.getIdent(var_ident); + // Add to scope + _ = try self.scopeIntroduceTypeVar(var_ident, new_anno_idx); - // Create qualified name: "module(a).Alias" - const qualified_text = try std.fmt.allocPrint(self.env.gpa, "module({s}).{s}", .{ var_name_text, alias_name_clean }); - defer self.env.gpa.free(qualified_text); - const qualified_name = try self.env.insertIdent(Ident.for_text(qualified_text)); + break :blk new_anno_idx; + }, + // Otherwise, this is malformed + .type_decl_anno => { + break :blk try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .undeclared_type_var = .{ + .name = var_ident, + .region = region, + } }); + }, + } + }, + }; - // Create module name: "module(a)" - const module_text = try std.fmt.allocPrint(self.env.gpa, "module({s})", .{var_name_text}); - defer self.env.gpa.free(module_text); - const module_name = try self.env.insertIdent(Ident.for_text(module_text)); + // Get alias being referenced + // where [ a.Alias ] + // ^^^^^ - const external_type_var = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .{ .flex_var = null }, region, TypeVar); - const external_decl = try self.createExternalDeclaration(qualified_name, module_name, alias_ident, .type, external_type_var, region); + const alias_ident = blk: { + // Resolve alias name (remove leading dot) + const alias_name_text = self.parse_ir.resolve(ma.name_tok); - return try self.env.addWhereClauseAndTypeVar(WhereClause{ .mod_alias = .{ - .var_name = var_ident, + // Remove leading dot from alias name + const alias_name_clean = if (alias_name_text.len > 0 and alias_name_text[0] == '.') + alias_name_text[1..] + else + alias_name_text; + + break :blk try self.env.insertIdent(Ident.for_text(alias_name_clean)); + }; + + return try self.env.addWhereClause(WhereClause{ .w_alias = .{ + .var_ = var_anno_idx, .alias_name = alias_ident, - .external_decl = external_decl, - } }, .{ .flex_var = null }, region); + } }, region); }, .malformed => |m| { const region = self.parse_ir.tokenizedRegionToRegion(m.region); - const diagnostic = try self.env.addDiagnosticAndTypeVar(Diagnostic{ .malformed_where_clause = .{ + const diagnostic = try self.env.addDiagnostic(Diagnostic{ .malformed_where_clause = .{ .region = region, - } }, .err); - return try self.env.addWhereClauseAndTypeVar(WhereClause{ .malformed = .{ + } }); + return try self.env.addWhereClause(WhereClause{ .w_malformed = .{ .diagnostic = diagnostic, - } }, .{ .flex_var = null }, region); + } }, region); }, } } /// Handle module-qualified types like Json.Decoder /// Create an annotation from a type annotation -fn createAnnotationFromTypeAnno(self: *Self, type_anno_idx: TypeAnno.Idx, region: Region) std.mem.Allocator.Error!?Annotation.Idx { +fn createAnnotationFromTypeAnno( + self: *Self, + type_anno_idx: TypeAnno.Idx, + mb_where_clauses: ?CIR.WhereClause.Span, + region: Region, +) std.mem.Allocator.Error!Annotation.Idx { const trace = tracy.trace(@src()); defer trace.end(); // Create the annotation structure - // TODO: Remove signature field from Annotation - const annotation = CIR.Annotation{ - .type_anno = type_anno_idx, - .signature = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .err, region, TypeVar), - }; + const annotation = CIR.Annotation{ .anno = type_anno_idx, .where = mb_where_clauses }; // Add to NodeStore and return the index - const annotation_idx = try self.env.addAnnotationAndTypeVarRedirect(annotation, ModuleEnv.varFrom(type_anno_idx), region); + const annotation_idx = try self.env.addAnnotation(annotation, region); return annotation_idx; } @@ -7017,16 +11025,108 @@ fn createAnnotationFromTypeAnno(self: *Self, type_anno_idx: TypeAnno.Idx, region /// we create external declarations that will be resolved later when /// we have access to the other module's IR after it has been type checked. fn processTypeImports(self: *Self, module_name: Ident.Idx, alias_name: Ident.Idx) std.mem.Allocator.Error!void { - // Set up the module alias for qualified lookups + // Set up the module alias for qualified lookups (type imports are not package-qualified) const scope = self.currentScope(); _ = try scope.introduceModuleAlias( self.env.gpa, alias_name, module_name, + false, // Type imports are not package-qualified null, // No parent lookup function for now ); } +/// Try to handle field access as a type variable alias dispatch. +/// +/// This handles cases like `Thing.method(args)` where `Thing` is a type variable alias +/// introduced by a statement like `Thing : thing` inside a function body. +/// +/// Returns `null` if this is not a type var alias dispatch. +fn tryTypeVarAliasDispatch(self: *Self, field_access: AST.BinOp) std.mem.Allocator.Error!?Expr.Idx { + const left_expr = self.parse_ir.store.getExpr(field_access.left); + if (left_expr != .ident) return null; + + const left_ident = left_expr.ident; + const alias_name = self.parse_ir.tokens.resolveIdentifier(left_ident.token) orelse return null; + + // Check if this is a type var alias in scope + const scope = self.currentScope(); + const lookup_result = scope.lookupTypeVarAlias(alias_name); + switch (lookup_result) { + .not_found => return null, + .found => |binding| { + // This is a type var alias! Handle the dispatch. + const region = self.parse_ir.tokenizedRegionToRegion(field_access.region); + const right_expr = self.parse_ir.store.getExpr(field_access.right); + + // Get the method name and arguments + switch (right_expr) { + .apply => |apply| { + // Case: `Thing.method(arg1, arg2)` + const method_expr = self.parse_ir.store.getExpr(apply.@"fn"); + if (method_expr != .ident) { + // Non-ident function in apply - malformed + return try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = region, + } }); + } + + const method_ident = method_expr.ident; + const method_name = self.parse_ir.tokens.resolveIdentifier(method_ident.token) orelse { + return try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = region, + } }); + }; + + // Canonicalize the arguments + const scratch_top = self.env.store.scratchExprTop(); + for (self.parse_ir.store.exprSlice(apply.args)) |arg_idx| { + if (try self.canonicalizeExpr(arg_idx)) |canonicalized| { + try self.env.store.addScratchExpr(canonicalized.get_idx()); + } + } + const args_span = try self.env.store.exprSpanFrom(scratch_top); + + // Create the type var dispatch expression + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_type_var_dispatch = .{ + .type_var_alias_stmt = binding.statement_idx, + .method_name = method_name, + .args = args_span, + }, + }, region); + return expr_idx; + }, + .ident => { + // Case: `Thing.method` (no arguments) + const right_ident = right_expr.ident; + const method_name = self.parse_ir.tokens.resolveIdentifier(right_ident.token) orelse { + return try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = region, + } }); + }; + + // Create the type var dispatch expression with empty args + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_type_var_dispatch = .{ + .type_var_alias_stmt = binding.statement_idx, + .method_name = method_name, + .args = .{ .span = DataSpan.empty() }, + }, + }, region); + return expr_idx; + }, + else => { + // Unexpected expression type on right side + return try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = region, + } }); + }, + } + }, + } +} + /// Try to handle field access as a module-qualified lookup. /// /// Examples: @@ -7042,12 +11142,24 @@ fn tryModuleQualifiedLookup(self: *Self, field_access: AST.BinOp) std.mem.Alloca const module_alias = self.parse_ir.tokens.resolveIdentifier(left_ident.token) orelse return null; // Check if this is a module alias - const module_name = self.scopeLookupModule(module_alias) orelse return null; + const module_info = self.scopeLookupModule(module_alias) orelse return null; + const module_name = module_info.module_name; const module_text = self.env.getIdent(module_name); // Check if this module is imported in the current scope - const import_idx = self.scopeLookupImportedModule(module_text) orelse { - // Module not imported in current scope + const import_idx = self.scopeLookupImportedModule(module_text) orelse blk: { + // Module not in import scope - check if it's an auto-imported module in module_envs + if (self.module_envs) |envs_map| { + if (envs_map.get(module_name)) |auto_imported_type| { + // This is an auto-imported module (like Bool, Try, Str, List, etc.) + // Use the ACTUAL module name from the environment, not the alias + // This ensures all auto-imported types from the same module share the same Import.Idx + const actual_module_name = auto_imported_type.env.module_name; + break :blk try self.getOrCreateAutoImport(actual_module_name); + } + } + + // Module not imported and not auto-imported const region = self.parse_ir.tokenizedRegionToRegion(field_access.region); _ = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .module_not_imported = .{ .module_name = module_name, @@ -7056,36 +11168,230 @@ fn tryModuleQualifiedLookup(self: *Self, field_access: AST.BinOp) std.mem.Alloca return null; }; - // This is a module-qualified lookup + // This IS a module-qualified lookup - we must handle it completely here. + // After this point, returning null would cause incorrect fallback to regular field access. const right_expr = self.parse_ir.store.getExpr(field_access.right); - if (right_expr != .ident) return null; - - const right_ident = right_expr.ident; - const field_name = self.parse_ir.tokens.resolveIdentifier(right_ident.token) orelse return null; - const region = self.parse_ir.tokenizedRegionToRegion(field_access.region); - // Look up the target node index in the module's exposed_items - // Need to convert identifier from current module to target module - const field_text = self.env.getIdent(field_name); - const target_node_idx = if (self.module_envs) |envs_map| blk: { - if (envs_map.get(module_text)) |module_env| { - if (module_env.common.findIdent(field_text)) |target_ident| { - break :blk module_env.getExposedNodeIndexById(target_ident) orelse 0; + // Handle method calls on module-qualified types (e.g., Stdout.line!(...)) + if (right_expr == .apply) { + const apply = right_expr.apply; + const method_expr = self.parse_ir.store.getExpr(apply.@"fn"); + if (method_expr != .ident) { + // Module-qualified call with non-ident function (e.g., Module.(complex_expr)(...)) + // This is malformed - report error + return try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = region, + } }); + } + + const method_ident = method_expr.ident; + const method_name = self.parse_ir.tokens.resolveIdentifier(method_ident.token) orelse { + // Couldn't resolve method name token + return try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = region, + } }); + }; + + // Check if this is a type module (like Stdout) - look up the qualified method name directly + if (self.module_envs) |envs_map| { + if (envs_map.get(module_name)) |auto_imported_type| { + if (auto_imported_type.statement_idx != null) { + // This is an imported type module (like Stdout) + // Look up the qualified method name (e.g., "Stdout.line!") in the module's exposed items + const module_env = auto_imported_type.env; + const module_name_text = module_env.module_name; + const auto_import_idx = try self.getOrCreateAutoImport(module_name_text); + + // Build the qualified method name: "TypeName.method_name" + const type_name_text = self.env.getIdent(module_name); + const method_name_text = self.env.getIdent(method_name); + const qualified_method_name = try self.env.insertQualifiedIdent(type_name_text, method_name_text); + const qualified_text = self.env.getIdent(qualified_method_name); + + // Look up the qualified method in the module's exposed items + if (module_env.common.findIdent(qualified_text)) |method_ident_idx| { + if (module_env.getExposedNodeIndexById(method_ident_idx)) |method_node_idx| { + // Found the method! Create e_lookup_external + e_call + const func_expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_external = .{ + .module_idx = auto_import_idx, + .target_node_idx = method_node_idx, + .region = region, + } }, region); + + // Canonicalize the arguments + const scratch_top = self.env.store.scratchExprTop(); + for (self.parse_ir.store.exprSlice(apply.args)) |arg_idx| { + if (try self.canonicalizeExpr(arg_idx)) |canonicalized| { + try self.env.store.addScratchExpr(canonicalized.get_idx()); + } + } + const args_span = try self.env.store.exprSpanFrom(scratch_top); + + // Create the call expression + const call_expr_idx = try self.env.addExpr(CIR.Expr{ + .e_call = .{ + .func = func_expr_idx, + .args = args_span, + .called_via = CalledVia.apply, + }, + }, region); + return call_expr_idx; + } + } + + // Method not found in module - generate error + return try self.env.pushMalformed(Expr.Idx, Diagnostic{ .nested_value_not_found = .{ + .parent_name = module_name, + .nested_name = method_name, + .region = region, + } }); + } + } + } + + // Module exists but is not a type module with a statement_idx - it's a regular module + // This means it's something like `SomeModule.someFunc(args)` where someFunc is a regular export + // We need to look up the function and create a call + const field_text = self.env.getIdent(method_name); + const target_node_idx_opt: ?u16 = if (self.module_envs) |envs_map| blk: { + if (envs_map.get(module_name)) |auto_imported_type| { + const module_env = auto_imported_type.env; + if (module_env.common.findIdent(field_text)) |target_ident| { + break :blk module_env.getExposedNodeIndexById(target_ident); + } else { + break :blk null; + } } else { - break :blk 0; + break :blk null; + } + } else null; + + if (target_node_idx_opt) |target_node_idx| { + // Found the function - create a lookup and call it + const func_expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_external = .{ + .module_idx = import_idx, + .target_node_idx = target_node_idx, + .region = region, + } }, region); + + // Canonicalize the arguments + const scratch_top = self.env.store.scratchExprTop(); + for (self.parse_ir.store.exprSlice(apply.args)) |arg_idx| { + if (try self.canonicalizeExpr(arg_idx)) |canonicalized| { + try self.env.store.addScratchExpr(canonicalized.get_idx()); + } + } + const args_span = try self.env.store.exprSpanFrom(scratch_top); + + // Create the call expression + const call_expr_idx = try self.env.addExpr(CIR.Expr{ + .e_call = .{ + .func = func_expr_idx, + .args = args_span, + .called_via = CalledVia.apply, + }, + }, region); + return call_expr_idx; + } else { + // Function not found in module + return try self.env.pushMalformed(Expr.Idx, Diagnostic{ .qualified_ident_does_not_exist = .{ + .ident = method_name, + .region = region, + } }); + } + } + + // Handle simple field access (not a method call) + if (right_expr != .ident) { + // Module-qualified access with non-ident, non-apply right side - malformed + return try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = region, + } }); + } + + const right_ident = right_expr.ident; + const field_name = self.parse_ir.tokens.resolveIdentifier(right_ident.token) orelse { + return try self.env.pushMalformed(Expr.Idx, Diagnostic{ .expr_not_canonicalized = .{ + .region = region, + } }); + }; + + // Check if this is a tag access on an auto-imported nominal type (e.g., Bool.True) + if (self.module_envs) |envs_map| { + if (envs_map.get(module_name)) |auto_imported_type| { + if (auto_imported_type.statement_idx) |stmt_idx| { + // This is an auto-imported nominal type with a statement index + // Treat field access as tag access (e.g., Bool.True) + // Create e_nominal_external to properly track the module origin + const module_name_text = auto_imported_type.env.module_name; + const auto_import_idx = try self.getOrCreateAutoImport(module_name_text); + + const target_node_idx = auto_imported_type.env.getExposedNodeIndexByStatementIdx(stmt_idx) orelse { + // Failed to find exposed node - return malformed expression with diagnostic + const module_ident = try self.env.insertIdent(base.Ident.for_text(module_name_text)); + return try self.env.pushMalformed(Expr.Idx, Diagnostic{ .nested_type_not_found = .{ + .parent_name = module_ident, + .nested_name = field_name, + .region = region, + } }); + }; + + // Create the tag expression + const tag_expr_idx = try self.env.addExpr(CIR.Expr{ + .e_tag = .{ + .name = field_name, + .args = Expr.Span{ .span = DataSpan.empty() }, + }, + }, region); + + // Wrap it in e_nominal_external to track the module + const expr_idx = try self.env.addExpr(CIR.Expr{ + .e_nominal_external = .{ + .module_idx = auto_import_idx, + .target_node_idx = target_node_idx, + .backing_expr = tag_expr_idx, + .backing_type = .tag, + }, + }, region); + return expr_idx; + } + } + } + + // Regular module-qualified lookup for definitions (not tags) + // Look up the target node index in the module's exposed_items + const field_text = self.env.getIdent(field_name); + const target_node_idx_opt: ?u16 = if (self.module_envs) |envs_map| blk: { + if (envs_map.get(module_name)) |auto_imported_type| { + const module_env = auto_imported_type.env; + if (module_env.common.findIdent(field_text)) |target_ident| { + // Found the identifier in the module - check if it's exposed + break :blk module_env.getExposedNodeIndexById(target_ident); + } else { + // The identifier doesn't exist in the module at all + break :blk null; } } else { - break :blk 0; + // Module not found in envs (shouldn't happen since we checked import_idx exists) + break :blk null; } - } else 0; + } else null; + + // If we didn't find a valid node index, report an error (don't fall back) + const target_node_idx = target_node_idx_opt orelse { + return try self.env.pushMalformed(Expr.Idx, Diagnostic{ .qualified_ident_does_not_exist = .{ + .ident = field_name, + .region = region, + } }); + }; // Create the e_lookup_external expression with Import.Idx - const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ .e_lookup_external = .{ + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_external = .{ .module_idx = import_idx, .target_node_idx = target_node_idx, .region = region, - } }, Content{ .flex_var = null }, region); + } }, region); return expr_idx; } @@ -7103,17 +11409,18 @@ fn canonicalizeRegularFieldAccess(self: *Self, field_access: AST.BinOp) std.mem. const receiver_idx = try self.canonicalizeFieldAccessReceiver(field_access) orelse return null; // Parse the right side - this could be just a field name or a method call - const field_name, const args = try self.parseFieldAccessRight(field_access); + const field_name, const field_name_region, const args = try self.parseFieldAccessRight(field_access); const dot_access_expr = CIR.Expr{ .e_dot_access = .{ .receiver = receiver_idx, .field_name = field_name, + .field_name_region = field_name_region, .args = args, }, }; - const expr_idx = try self.env.addExprAndTypeVar(dot_access_expr, Content{ .flex_var = null }, self.parse_ir.tokenizedRegionToRegion(field_access.region)); + const expr_idx = try self.env.addExpr(dot_access_expr, self.parse_ir.tokenizedRegionToRegion(field_access.region)); return expr_idx; } @@ -7141,16 +11448,24 @@ fn canonicalizeFieldAccessReceiver(self: *Self, field_access: AST.BinOp) std.mem /// Parse the right side of field access, handling both plain fields and method calls. /// /// Examples: -/// - `user.name` - returns `("name", null)` for plain field access -/// - `list.map(fn)` - returns `("map", args)` where args contains the canonicalized function -/// - `obj.method(a, b)` - returns `("method", args)` where args contains canonicalized a and b -fn parseFieldAccessRight(self: *Self, field_access: AST.BinOp) std.mem.Allocator.Error!struct { Ident.Idx, ?Expr.Span } { +/// - `user.name` - returns `("name", region, null)` for plain field access +/// - `list.map(fn)` - returns `("map", region, args)` where args contains the canonicalized function +/// - `obj.method(a, b)` - returns `("method", region, args)` where args contains canonicalized a and b +fn parseFieldAccessRight(self: *Self, field_access: AST.BinOp) std.mem.Allocator.Error!struct { Ident.Idx, Region, ?Expr.Span } { const right_expr = self.parse_ir.store.getExpr(field_access.right); return switch (right_expr) { .apply => |apply| try self.parseMethodCall(apply), - .ident => |ident| .{ try self.resolveIdentOrFallback(ident.token), null }, - else => .{ try self.createUnknownIdent(), null }, + .ident => |ident| .{ + try self.resolveIdentOrFallback(ident.token), + self.parse_ir.tokenizedRegionToRegion(ident.region), + null, + }, + else => .{ + try self.createUnknownIdent(), + self.parse_ir.tokenizedRegionToRegion(field_access.region), // fallback to whole region + null, + }, }; } @@ -7160,11 +11475,25 @@ fn parseFieldAccessRight(self: *Self, field_access: AST.BinOp) std.mem.Allocator /// - `.map(transform)` - extracts "map" as method name and canonicalizes `transform` argument /// - `.filter(predicate)` - extracts "filter" and canonicalizes `predicate` /// - `.fold(0, combine)` - extracts "fold" and canonicalizes both `0` and `combine` arguments -fn parseMethodCall(self: *Self, apply: @TypeOf(@as(AST.Expr, undefined).apply)) std.mem.Allocator.Error!struct { Ident.Idx, ?Expr.Span } { +fn parseMethodCall(self: *Self, apply: @TypeOf(@as(AST.Expr, undefined).apply)) std.mem.Allocator.Error!struct { Ident.Idx, Region, ?Expr.Span } { const method_expr = self.parse_ir.store.getExpr(apply.@"fn"); - const field_name = switch (method_expr) { - .ident => |ident| try self.resolveIdentOrFallback(ident.token), - else => try self.createUnknownIdent(), + const field_name, const field_name_region = switch (method_expr) { + .ident => |ident| blk: { + const raw_region = self.parse_ir.tokenizedRegionToRegion(ident.region); + // Skip the leading dot if present (parser includes it in ident region for field access) + const adjusted_region = if (raw_region.end.offset > raw_region.start.offset) + Region{ .start = .{ .offset = raw_region.start.offset + 1 }, .end = raw_region.end } + else + raw_region; + break :blk .{ + try self.resolveIdentOrFallback(ident.token), + adjusted_region, + }; + }, + else => .{ + try self.createUnknownIdent(), + self.parse_ir.tokenizedRegionToRegion(apply.region), // fallback + }, }; // Canonicalize the arguments using scratch system @@ -7174,12 +11503,12 @@ fn parseMethodCall(self: *Self, apply: @TypeOf(@as(AST.Expr, undefined).apply)) try self.env.store.addScratchExpr(canonicalized.get_idx()); } else { self.env.store.clearScratchExprsFrom(scratch_top); - return .{ field_name, null }; + return .{ field_name, field_name_region, null }; } } const args = try self.env.store.exprSpanFrom(scratch_top); - return .{ field_name, args }; + return .{ field_name, field_name_region, args }; } /// Resolve an identifier token or return a fallback "unknown" identifier. @@ -7206,6 +11535,127 @@ fn createUnknownIdent(self: *Self) std.mem.Allocator.Error!Ident.Idx { return try self.env.insertIdent(base.Ident.for_text("unknown")); } +const MainFunctionStatus = enum { valid, invalid, not_found }; + +/// Check if this module has a valid main! function (1 argument lambda). +/// Reports an error if main! exists but has the wrong arity. +fn checkMainFunction(self: *Self) std.mem.Allocator.Error!MainFunctionStatus { + const file = self.parse_ir.store.getFile(); + + for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + if (stmt == .decl) { + const decl = stmt.decl; + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const ident_token = pattern.ident.ident_tok; + const ident_idx = self.parse_ir.tokens.resolveIdentifier(ident_token) orelse continue; + const ident_text = self.env.getIdent(ident_idx); + + if (std.mem.eql(u8, ident_text, "main!")) { + const region = self.parse_ir.tokenizedRegionToRegion(decl.region); + const expr = self.parse_ir.store.getExpr(decl.body); + + if (expr == .lambda) { + const lambda = expr.lambda; + const params = self.parse_ir.store.patternSlice(lambda.args); + + if (params.len == 1) { + return .valid; + } else { + try self.env.pushDiagnostic(Diagnostic{ .default_app_wrong_arity = .{ + .arity = @intCast(params.len), + .region = region, + } }); + return .invalid; + } + } + } + } + } + } + + return .not_found; +} + +/// Check if there's a type declaration matching the module name +/// Find the type declaration matching the module name and return its ident +fn findMatchingTypeIdent(self: *Self) ?Ident.Idx { + const file = self.parse_ir.store.getFile(); + const module_name_text = self.env.module_name; + + // Look through all statements for a type declaration matching the module name + for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + // Get the type name from the header + const header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; + const type_name_ident = self.parse_ir.tokens.resolveIdentifier(header.name) orelse continue; + const type_name_text = self.env.getIdent(type_name_ident); + + if (std.mem.eql(u8, type_name_text, module_name_text)) { + return type_name_ident; + } + } + } + + return null; +} + +/// Check if any type declarations exist in the file +fn hasAnyTypeDeclarations(self: *Self) bool { + const file = self.parse_ir.store.getFile(); + + for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + if (stmt == .type_decl) { + return true; + } + } + + return false; +} + +/// Report smart error when neither type module nor default-app is valid (checking mode) +fn reportTypeModuleOrDefaultAppError(self: *Self) std.mem.Allocator.Error!void { + const file = self.parse_ir.store.getFile(); + const module_name_text = self.env.module_name; + const module_name_ident = try self.env.insertIdent(base.Ident.for_text(module_name_text)); + const file_region = self.parse_ir.tokenizedRegionToRegion(file.region); + + // Use heuristic: if there are types declared, assume type module, else assume default-app + if (self.hasAnyTypeDeclarations()) { + // Assume user wanted type module + try self.env.pushDiagnostic(.{ + .type_module_missing_matching_type = .{ + .module_name = module_name_ident, + .region = file_region, + }, + }); + } else { + // Assume user wanted default-app + try self.env.pushDiagnostic(.{ + .default_app_missing_main = .{ + .module_name = module_name_ident, + .region = file_region, + }, + }); + } +} + +/// Report error when trying to execute a plain type module +fn reportExecutionRequiresAppOrDefaultApp(self: *Self) std.mem.Allocator.Error!void { + const file = self.parse_ir.store.getFile(); + const file_region = self.parse_ir.tokenizedRegionToRegion(file.region); + + try self.env.pushDiagnostic(.{ + .execution_requires_app_or_default_app = .{ + .region = file_region, + }, + }); +} + // We write out this giant literal because it's actually annoying to try to // take std.math.minInt(i128), drop the minus sign, and convert it to u128 // all at comptime. Instead we just have a test that verifies its correctness. diff --git a/src/canonicalize/ClosureTransformer.zig b/src/canonicalize/ClosureTransformer.zig new file mode 100644 index 0000000000..2e47f32c28 --- /dev/null +++ b/src/canonicalize/ClosureTransformer.zig @@ -0,0 +1,847 @@ +//! Closure Transformer +//! +//! Transforms closures with captures into tagged values with explicit capture records. +//! This is the first step of lambda set specialization following the Cor approach. +//! +//! ## Transformation Example +//! +//! Input: +//! ```roc +//! { +//! x = 42 +//! addX = |y| x + y +//! addX(10) +//! } +//! ``` +//! +//! Output: +//! ```roc +//! { +//! x = 42 +//! addX = #addX({ x: x }) +//! match addX { +//! #addX({ x }) => { +//! y = 10 +//! (x + y) +//! }, +//! } +//! } +//! ``` +//! +//! ## Implementation Notes +//! +//! - Closures become tags with capture records (using `#` prefix to avoid clashing with userspace tags) +//! - Call sites become inline match expressions that dispatch based on the lambda set +//! - Pure lambdas (no captures) become tags with empty records + +const std = @import("std"); +const base = @import("base"); + +const ModuleEnv = @import("ModuleEnv.zig"); +const CIR = @import("CIR.zig"); +const Expr = CIR.Expr; +const Pattern = @import("Pattern.zig").Pattern; +const RecordField = CIR.RecordField; + +const Self = @This(); + +/// Information about a transformed closure +pub const ClosureInfo = struct { + /// The tag name for this closure (e.g., `addX`) + tag_name: base.Ident.Idx, + /// The lambda body expression + lambda_body: Expr.Idx, + /// The lambda arguments + lambda_args: CIR.Pattern.Span, + /// The capture names (for generating dispatch function patterns) + capture_names: std.ArrayList(base.Ident.Idx), +}; + +/// Information for generating a dispatch function +pub const DispatchFunction = struct { + /// Name of the dispatch function (e.g., `call_addX`) + name: base.Ident.Idx, + /// The closures that can reach this call site + closures: std.ArrayList(ClosureInfo), +}; + +/// The allocator for intermediate allocations +allocator: std.mem.Allocator, + +/// The module environment containing the CIR (mutable for adding new expressions) +module_env: *ModuleEnv, + +/// Counter for generating unique closure names +closure_counter: u32, + +/// Map from original closure expression to its transformation info +closures: std.AutoHashMap(Expr.Idx, ClosureInfo), + +/// Map from pattern index to closure info (for tracking which variables hold closures) +pattern_closures: std.AutoHashMap(CIR.Pattern.Idx, ClosureInfo), + +/// List of dispatch functions to generate +dispatch_functions: std.ArrayList(DispatchFunction), + +/// Initialize the transformer +pub fn init(allocator: std.mem.Allocator, module_env: *ModuleEnv) Self { + return .{ + .allocator = allocator, + .module_env = module_env, + .closure_counter = 0, + .closures = std.AutoHashMap(Expr.Idx, ClosureInfo).init(allocator), + .pattern_closures = std.AutoHashMap(CIR.Pattern.Idx, ClosureInfo).init(allocator), + .dispatch_functions = std.ArrayList(DispatchFunction).empty, + }; +} + +/// Free resources +pub fn deinit(self: *Self) void { + // Free capture name lists + var closure_iter = self.closures.valueIterator(); + while (closure_iter.next()) |info| { + info.capture_names.deinit(self.allocator); + } + self.closures.deinit(); + + // pattern_closures shares ClosureInfo with closures, don't double-free + self.pattern_closures.deinit(); + + // Free dispatch function closure lists + for (self.dispatch_functions.items) |*df| { + df.closures.deinit(self.allocator); + } + self.dispatch_functions.deinit(self.allocator); +} + +/// Generate a unique tag name for a closure +pub fn generateClosureTagName(self: *Self, hint: ?base.Ident.Idx) !base.Ident.Idx { + self.closure_counter += 1; + + // If we have a hint (e.g., from the variable name), use it + if (hint) |h| { + const hint_name = self.module_env.getIdent(h); + // Use # prefix since it's Roc's comment syntax and can't clash with userspace tags + // e.g., "myFunc" becomes "#myFunc" + const tag_name = try std.fmt.allocPrint( + self.allocator, + "#{s}", + .{hint_name}, + ); + defer self.allocator.free(tag_name); + return try self.module_env.insertIdent(base.Ident.for_text(tag_name)); + } + + // Otherwise generate a numeric name + const tag_name = try std.fmt.allocPrint( + self.allocator, + "#{d}", + .{self.closure_counter}, + ); + defer self.allocator.free(tag_name); + return try self.module_env.insertIdent(base.Ident.for_text(tag_name)); +} + +/// Generate a dispatch match expression for a closure call. +/// +/// Transforms a call like `f(10)` where `f` is a closure into: +/// ```roc +/// match f { +/// #f({ x }) => { +/// y = 10 # Bind call arguments to lambda parameters +/// x + y # Original lambda body +/// } +/// } +/// ``` +fn generateDispatchMatch( + self: *Self, + closure_var_expr: Expr.Idx, + closure_info: ClosureInfo, + call_args: []const Expr.Idx, +) !Expr.Idx { + // Step 1: Create the capture record destructure pattern + // For `{ x, y }` we need a record_destructure with each field + + const record_destruct_start = self.module_env.store.scratchRecordDestructTop(); + + for (closure_info.capture_names.items) |capture_name| { + // Create an assign pattern for this capture binding + const assign_pattern = try self.module_env.store.addPattern( + Pattern{ .assign = .{ .ident = capture_name } }, + base.Region.zero(), + ); + + // Create the record destruct for this field + const destruct = Pattern.RecordDestruct{ + .label = capture_name, + .ident = capture_name, + .kind = .{ .Required = assign_pattern }, + }; + const destruct_idx = try self.module_env.store.addRecordDestruct(destruct, base.Region.zero()); + try self.module_env.store.addScratchRecordDestruct(destruct_idx); + } + + const destructs_span = try self.module_env.store.recordDestructSpanFrom(record_destruct_start); + + // Create the record destructure pattern + const record_pattern = try self.module_env.store.addPattern( + Pattern{ .record_destructure = .{ .destructs = destructs_span } }, + base.Region.zero(), + ); + + // Step 2: Create the applied_tag pattern: `f({ x, y }) + // The tag pattern takes the record pattern as its single argument + const pattern_args_start = self.module_env.store.scratchPatternTop(); + try self.module_env.store.addScratchPattern(record_pattern); + const pattern_args_span = try self.module_env.store.patternSpanFrom(pattern_args_start); + + const tag_pattern = try self.module_env.store.addPattern( + Pattern{ .applied_tag = .{ + .name = closure_info.tag_name, + .args = pattern_args_span, + } }, + base.Region.zero(), + ); + + // Step 3: Create the body - a block that binds arguments then executes lambda body + // We need to bind each call argument to the corresponding lambda parameter + + const lambda_params = self.module_env.store.slicePatterns(closure_info.lambda_args); + + // If we have arguments to bind, create a block with let bindings + const body_expr = if (call_args.len > 0 and lambda_params.len > 0) blk: { + const stmt_start = self.module_env.store.scratch.?.statements.top(); + + // Bind each argument to its parameter + const num_args = @min(call_args.len, lambda_params.len); + for (0..num_args) |i| { + const param_pattern = lambda_params[i]; + const arg_expr = call_args[i]; + + const stmt = CIR.Statement{ .s_decl = .{ + .pattern = param_pattern, + .expr = arg_expr, + .anno = null, + } }; + const stmt_idx = try self.module_env.store.addStatement(stmt, base.Region.zero()); + try self.module_env.store.scratch.?.statements.append(stmt_idx); + } + + const stmts_span = try self.module_env.store.statementSpanFrom(stmt_start); + + // Create block with bindings and lambda body as final expression + break :blk try self.module_env.store.addExpr(Expr{ + .e_block = .{ + .stmts = stmts_span, + .final_expr = closure_info.lambda_body, + }, + }, base.Region.zero()); + } else blk: { + // No arguments, just use the lambda body directly + break :blk closure_info.lambda_body; + }; + + // Step 4: Create the match branch + const branch_pattern_start = self.module_env.store.scratchMatchBranchPatternTop(); + const branch_pattern = try self.module_env.store.addMatchBranchPattern( + Expr.Match.BranchPattern{ + .pattern = tag_pattern, + .degenerate = false, + }, + base.Region.zero(), + ); + try self.module_env.store.addScratchMatchBranchPattern(branch_pattern); + const branch_patterns_span = try self.module_env.store.matchBranchPatternSpanFrom(branch_pattern_start); + + // Create a fresh type variable for the redundant field + const redundant_var = try self.module_env.types.fresh(); + + const branch = Expr.Match.Branch{ + .patterns = branch_patterns_span, + .value = body_expr, + .guard = null, + .redundant = redundant_var, + }; + const branch_idx = try self.module_env.store.addMatchBranch(branch, base.Region.zero()); + + // Step 5: Create the match expression + const branch_start = self.module_env.store.scratchMatchBranchTop(); + try self.module_env.store.addScratchMatchBranch(branch_idx); + const branches_span = try self.module_env.store.matchBranchSpanFrom(branch_start); + + // Create a fresh type variable for exhaustiveness + const exhaustive_var = try self.module_env.types.fresh(); + + return try self.module_env.store.addExpr(Expr{ + .e_match = .{ + .cond = closure_var_expr, + .branches = branches_span, + .exhaustive = exhaustive_var, + }, + }, base.Region.zero()); +} + +/// Transform a closure expression into a tag with capture record. +/// Returns the new expression index. +pub fn transformClosure( + self: *Self, + closure_expr_idx: Expr.Idx, + binding_name_hint: ?base.Ident.Idx, +) !Expr.Idx { + const expr = self.module_env.store.getExpr(closure_expr_idx); + + switch (expr) { + .e_closure => |closure| { + // Get the lambda body and args + const lambda_expr = self.module_env.store.getExpr(closure.lambda_idx); + const lambda = switch (lambda_expr) { + .e_lambda => |l| l, + else => return closure_expr_idx, // Not a lambda, return as-is + }; + + // Generate tag name + const tag_name = try self.generateClosureTagName(binding_name_hint); + + // Get captures + const captures = self.module_env.store.sliceCaptures(closure.captures); + + // Build capture record fields + const scratch_top = self.module_env.store.scratch.?.record_fields.top(); + + var capture_names = std.ArrayList(base.Ident.Idx).empty; + + for (captures) |capture_idx| { + const capture = self.module_env.store.getCapture(capture_idx); + + // Create a lookup expression for the captured variable + // Use store.addExpr directly to avoid region sync checks during transformation + const lookup_expr = try self.module_env.store.addExpr(Expr{ + .e_lookup_local = .{ .pattern_idx = capture.pattern_idx }, + }, base.Region.zero()); + + // Create record field: { capture_name: capture_value } + const field = RecordField{ + .name = capture.name, + .value = lookup_expr, + }; + const field_idx = try self.module_env.store.addRecordField(field, base.Region.zero()); + try self.module_env.store.scratch.?.record_fields.append(field_idx); + try capture_names.append(self.allocator, capture.name); + } + + // Create the record expression + const fields_span = try self.module_env.store.recordFieldSpanFrom(scratch_top); + + const record_expr = if (captures.len > 0) + try self.module_env.store.addExpr(Expr{ + .e_record = .{ .fields = fields_span, .ext = null }, + }, base.Region.zero()) + else + try self.module_env.store.addExpr(Expr{ + .e_empty_record = .{}, + }, base.Region.zero()); + + // Create the tag expression: `tagName(captureRecord) + // First, add the record as an argument + const args_start = self.module_env.store.scratch.?.exprs.top(); + try self.module_env.store.scratch.?.exprs.append(record_expr); + const args_span = try self.module_env.store.exprSpanFrom(args_start); + + const tag_expr = try self.module_env.store.addExpr(Expr{ + .e_tag = .{ + .name = tag_name, + .args = args_span, + }, + }, base.Region.zero()); + + // Store closure info for dispatch function generation + try self.closures.put(closure_expr_idx, ClosureInfo{ + .tag_name = tag_name, + .lambda_body = lambda.body, + .lambda_args = lambda.args, + .capture_names = capture_names, + }); + + return tag_expr; + }, + .e_lambda => |lambda| { + // Pure lambda (no captures) - still wrap in a tag with empty record + const tag_name = try self.generateClosureTagName(binding_name_hint); + + const empty_record = try self.module_env.store.addExpr(Expr{ + .e_empty_record = .{}, + }, base.Region.zero()); + + const args_start = self.module_env.store.scratch.?.exprs.top(); + try self.module_env.store.scratch.?.exprs.append(empty_record); + const args_span = try self.module_env.store.exprSpanFrom(args_start); + + const tag_expr = try self.module_env.store.addExpr(Expr{ + .e_tag = .{ + .name = tag_name, + .args = args_span, + }, + }, base.Region.zero()); + + // Store info for dispatch + try self.closures.put(closure_expr_idx, ClosureInfo{ + .tag_name = tag_name, + .lambda_body = lambda.body, + .lambda_args = lambda.args, + .capture_names = std.ArrayList(base.Ident.Idx).empty, + }); + + return tag_expr; + }, + else => return closure_expr_idx, // Not a closure, return as-is + } +} + +/// Transform an entire expression tree, handling closures and their call sites. +/// This is the main entry point for the transformation. +pub fn transformExpr(self: *Self, expr_idx: Expr.Idx) !Expr.Idx { + const expr = self.module_env.store.getExpr(expr_idx); + + switch (expr) { + .e_closure => { + // Transform closure to tag + return try self.transformClosure(expr_idx, null); + }, + .e_lambda => { + // Transform pure lambda to tag + return try self.transformClosure(expr_idx, null); + }, + .e_block => |block| { + // Transform block: handle statements and final expression + const stmts = self.module_env.store.sliceStatements(block.stmts); + + // Create new statements with transformed expressions + const stmt_start = self.module_env.store.scratch.?.statements.top(); + + for (stmts) |stmt_idx| { + const stmt = self.module_env.store.getStatement(stmt_idx); + switch (stmt) { + .s_decl => |decl| { + // Get binding name hint from pattern + const pattern = self.module_env.store.getPattern(decl.pattern); + const name_hint: ?base.Ident.Idx = switch (pattern) { + .assign => |a| a.ident, + else => null, + }; + + // Check if this is a closure binding + const decl_expr = self.module_env.store.getExpr(decl.expr); + const new_expr = switch (decl_expr) { + .e_closure, .e_lambda => blk: { + const transformed = try self.transformClosure(decl.expr, name_hint); + // Track this pattern as holding a closure + if (self.closures.get(decl.expr)) |closure_info| { + try self.pattern_closures.put(decl.pattern, closure_info); + } + break :blk transformed; + }, + else => try self.transformExpr(decl.expr), + }; + + // Create new statement with transformed expression + const new_stmt_idx = try self.module_env.store.addStatement( + CIR.Statement{ .s_decl = .{ + .pattern = decl.pattern, + .expr = new_expr, + .anno = decl.anno, + } }, + base.Region.zero(), + ); + try self.module_env.store.scratch.?.statements.append(new_stmt_idx); + }, + .s_decl_gen => |decl| { + const pattern = self.module_env.store.getPattern(decl.pattern); + const name_hint: ?base.Ident.Idx = switch (pattern) { + .assign => |a| a.ident, + else => null, + }; + + const decl_expr = self.module_env.store.getExpr(decl.expr); + const new_expr = switch (decl_expr) { + .e_closure, .e_lambda => blk: { + const transformed = try self.transformClosure(decl.expr, name_hint); + // Track this pattern as holding a closure + if (self.closures.get(decl.expr)) |closure_info| { + try self.pattern_closures.put(decl.pattern, closure_info); + } + break :blk transformed; + }, + else => try self.transformExpr(decl.expr), + }; + + const new_stmt_idx = try self.module_env.store.addStatement( + CIR.Statement{ .s_decl_gen = .{ + .pattern = decl.pattern, + .expr = new_expr, + .anno = decl.anno, + } }, + base.Region.zero(), + ); + try self.module_env.store.scratch.?.statements.append(new_stmt_idx); + }, + else => { + // Copy statement as-is + try self.module_env.store.scratch.?.statements.append(stmt_idx); + }, + } + } + + const new_stmts_span = try self.module_env.store.statementSpanFrom(stmt_start); + + // Transform final expression + const new_final = try self.transformExpr(block.final_expr); + + // Create new block + return try self.module_env.store.addExpr(Expr{ + .e_block = .{ + .stmts = new_stmts_span, + .final_expr = new_final, + }, + }, base.Region.zero()); + }, + .e_call => |call| { + // First transform arguments recursively + const args = self.module_env.store.sliceExpr(call.args); + const args_start = self.module_env.store.scratch.?.exprs.top(); + + for (args) |arg_idx| { + const new_arg = try self.transformExpr(arg_idx); + try self.module_env.store.scratch.?.exprs.append(new_arg); + } + + const new_args_span = try self.module_env.store.exprSpanFrom(args_start); + const transformed_args = self.module_env.store.sliceExpr(new_args_span); + + // Check if the function is a local variable that holds a closure + const func_expr = self.module_env.store.getExpr(call.func); + switch (func_expr) { + .e_lookup_local => |lookup| { + // Check if this pattern was assigned a closure + if (self.pattern_closures.get(lookup.pattern_idx)) |closure_info| { + // Generate a dispatch match expression + return try self.generateDispatchMatch( + call.func, + closure_info, + transformed_args, + ); + } + }, + else => {}, + } + + // Not a closure call, transform normally + const new_func = try self.transformExpr(call.func); + + return try self.module_env.store.addExpr(Expr{ + .e_call = .{ + .func = new_func, + .args = new_args_span, + .called_via = call.called_via, + }, + }, base.Region.zero()); + }, + .e_if => |if_expr| { + const branches = self.module_env.store.sliceIfBranches(if_expr.branches); + const branch_start = self.module_env.store.scratch.?.if_branches.top(); + + for (branches) |branch_idx| { + const branch = self.module_env.store.getIfBranch(branch_idx); + const new_cond = try self.transformExpr(branch.cond); + const new_body = try self.transformExpr(branch.body); + + const new_branch_idx = try self.module_env.store.addIfBranch( + Expr.IfBranch{ .cond = new_cond, .body = new_body }, + base.Region.zero(), + ); + try self.module_env.store.scratch.?.if_branches.append(new_branch_idx); + } + + const new_branches_span = try self.module_env.store.ifBranchSpanFrom(branch_start); + const new_else = try self.transformExpr(if_expr.final_else); + + return try self.module_env.store.addExpr(Expr{ + .e_if = .{ + .branches = new_branches_span, + .final_else = new_else, + }, + }, base.Region.zero()); + }, + .e_binop => |binop| { + const new_lhs = try self.transformExpr(binop.lhs); + const new_rhs = try self.transformExpr(binop.rhs); + + return try self.module_env.store.addExpr(Expr{ + .e_binop = .{ + .op = binop.op, + .lhs = new_lhs, + .rhs = new_rhs, + }, + }, base.Region.zero()); + }, + // Pass through simple expressions unchanged + .e_num, + .e_frac_f32, + .e_frac_f64, + .e_dec, + .e_dec_small, + .e_str_segment, + .e_str, + .e_lookup_local, + .e_lookup_external, + .e_empty_list, + .e_empty_record, + .e_zero_argument_tag, + .e_runtime_error, + .e_ellipsis, + .e_anno_only, + .e_lookup_required, + .e_type_var_dispatch, + .e_hosted_lambda, + .e_low_level_lambda, + => return expr_idx, + + .e_list => |list| { + const elems = self.module_env.store.sliceExpr(list.elems); + const elems_start = self.module_env.store.scratch.?.exprs.top(); + + for (elems) |elem_idx| { + const new_elem = try self.transformExpr(elem_idx); + try self.module_env.store.scratch.?.exprs.append(new_elem); + } + + const new_elems_span = try self.module_env.store.exprSpanFrom(elems_start); + + return try self.module_env.store.addExpr(Expr{ + .e_list = .{ .elems = new_elems_span }, + }, base.Region.zero()); + }, + .e_tuple => |tuple| { + const elems = self.module_env.store.sliceExpr(tuple.elems); + const elems_start = self.module_env.store.scratch.?.exprs.top(); + + for (elems) |elem_idx| { + const new_elem = try self.transformExpr(elem_idx); + try self.module_env.store.scratch.?.exprs.append(new_elem); + } + + const new_elems_span = try self.module_env.store.exprSpanFrom(elems_start); + + return try self.module_env.store.addExpr(Expr{ + .e_tuple = .{ .elems = new_elems_span }, + }, base.Region.zero()); + }, + .e_record => |record| { + const field_indices = self.module_env.store.sliceRecordFields(record.fields); + const fields_start = self.module_env.store.scratch.?.record_fields.top(); + + for (field_indices) |field_idx| { + const field = self.module_env.store.getRecordField(field_idx); + const new_value = try self.transformExpr(field.value); + + const new_field = RecordField{ + .name = field.name, + .value = new_value, + }; + const new_field_idx = try self.module_env.store.addRecordField(new_field, base.Region.zero()); + try self.module_env.store.scratch.?.record_fields.append(new_field_idx); + } + + const new_fields_span = try self.module_env.store.recordFieldSpanFrom(fields_start); + + const new_ext = if (record.ext) |ext| try self.transformExpr(ext) else null; + + return try self.module_env.store.addExpr(Expr{ + .e_record = .{ + .fields = new_fields_span, + .ext = new_ext, + }, + }, base.Region.zero()); + }, + .e_tag => |tag| { + const args = self.module_env.store.sliceExpr(tag.args); + const args_start = self.module_env.store.scratch.?.exprs.top(); + + for (args) |arg_idx| { + const new_arg = try self.transformExpr(arg_idx); + try self.module_env.store.scratch.?.exprs.append(new_arg); + } + + const new_args_span = try self.module_env.store.exprSpanFrom(args_start); + + return try self.module_env.store.addExpr(Expr{ + .e_tag = .{ + .name = tag.name, + .args = new_args_span, + }, + }, base.Region.zero()); + }, + .e_unary_minus => |unary| { + const new_expr = try self.transformExpr(unary.expr); + return try self.module_env.store.addExpr(Expr{ + .e_unary_minus = .{ .expr = new_expr }, + }, base.Region.zero()); + }, + .e_unary_not => |unary| { + const new_expr = try self.transformExpr(unary.expr); + return try self.module_env.store.addExpr(Expr{ + .e_unary_not = .{ .expr = new_expr }, + }, base.Region.zero()); + }, + .e_dot_access => |dot| { + const new_receiver = try self.transformExpr(dot.receiver); + const new_args = if (dot.args) |args_span| blk: { + const args = self.module_env.store.sliceExpr(args_span); + const args_start = self.module_env.store.scratch.?.exprs.top(); + + for (args) |arg_idx| { + const new_arg = try self.transformExpr(arg_idx); + try self.module_env.store.scratch.?.exprs.append(new_arg); + } + + break :blk try self.module_env.store.exprSpanFrom(args_start); + } else null; + + return try self.module_env.store.addExpr(Expr{ + .e_dot_access = .{ + .receiver = new_receiver, + .field_name = dot.field_name, + .field_name_region = dot.field_name_region, + .args = new_args, + }, + }, base.Region.zero()); + }, + .e_crash => return expr_idx, + .e_dbg => |dbg| { + const new_expr = try self.transformExpr(dbg.expr); + return try self.module_env.store.addExpr(Expr{ + .e_dbg = .{ + .expr = new_expr, + }, + }, base.Region.zero()); + }, + .e_expect => |expect| { + const new_body = try self.transformExpr(expect.body); + return try self.module_env.store.addExpr(Expr{ + .e_expect = .{ + .body = new_body, + }, + }, base.Region.zero()); + }, + .e_return => |ret| { + const new_expr = try self.transformExpr(ret.expr); + return try self.module_env.store.addExpr(Expr{ + .e_return = .{ .expr = new_expr }, + }, base.Region.zero()); + }, + .e_match => |match| { + const new_cond = try self.transformExpr(match.cond); + // Note: match branches would need deeper transformation for closures in branches + // For now, pass through as-is + return try self.module_env.store.addExpr(Expr{ + .e_match = .{ + .cond = new_cond, + .branches = match.branches, + .exhaustive = match.exhaustive, + }, + }, base.Region.zero()); + }, + .e_nominal => |nominal| { + const new_backing = try self.transformExpr(nominal.backing_expr); + return try self.module_env.store.addExpr(Expr{ + .e_nominal = .{ + .nominal_type_decl = nominal.nominal_type_decl, + .backing_expr = new_backing, + .backing_type = nominal.backing_type, + }, + }, base.Region.zero()); + }, + .e_nominal_external => |nominal| { + const new_backing = try self.transformExpr(nominal.backing_expr); + return try self.module_env.store.addExpr(Expr{ + .e_nominal_external = .{ + .module_idx = nominal.module_idx, + .target_node_idx = nominal.target_node_idx, + .backing_expr = new_backing, + .backing_type = nominal.backing_type, + }, + }, base.Region.zero()); + }, + .e_for => |for_expr| { + const new_expr = try self.transformExpr(for_expr.expr); + const new_body = try self.transformExpr(for_expr.body); + return try self.module_env.store.addExpr(Expr{ + .e_for = .{ + .patt = for_expr.patt, + .expr = new_expr, + .body = new_body, + }, + }, base.Region.zero()); + }, + } +} + +// Tests + +const testing = std.testing; + +test "ClosureTransformer: init and deinit" { + const allocator = testing.allocator; + + const module_env = try allocator.create(ModuleEnv); + module_env.* = try ModuleEnv.init(allocator, "test"); + defer { + module_env.deinit(); + allocator.destroy(module_env); + } + + var transformer = Self.init(allocator, module_env); + defer transformer.deinit(); + + try testing.expectEqual(@as(u32, 0), transformer.closure_counter); +} + +test "ClosureTransformer: generateClosureTagName with hint" { + const allocator = testing.allocator; + + const module_env = try allocator.create(ModuleEnv); + module_env.* = try ModuleEnv.init(allocator, "test"); + defer { + module_env.deinit(); + allocator.destroy(module_env); + } + + var transformer = Self.init(allocator, module_env); + defer transformer.deinit(); + + // Create a hint identifier + const hint = try module_env.insertIdent(base.Ident.for_text("addX")); + + const tag_name = try transformer.generateClosureTagName(hint); + const tag_str = module_env.getIdent(tag_name); + + try testing.expectEqualStrings("#addX", tag_str); +} + +test "ClosureTransformer: generateClosureTagName without hint" { + const allocator = testing.allocator; + + const module_env = try allocator.create(ModuleEnv); + module_env.* = try ModuleEnv.init(allocator, "test"); + defer { + module_env.deinit(); + allocator.destroy(module_env); + } + + var transformer = Self.init(allocator, module_env); + defer transformer.deinit(); + + const tag_name = try transformer.generateClosureTagName(null); + const tag_str = module_env.getIdent(tag_name); + + try testing.expectEqualStrings("#1", tag_str); +} diff --git a/src/canonicalize/DependencyGraph.zig b/src/canonicalize/DependencyGraph.zig new file mode 100644 index 0000000000..6f899d40e0 --- /dev/null +++ b/src/canonicalize/DependencyGraph.zig @@ -0,0 +1,484 @@ +//! Dependency Graph and SCC computation for top-level definitions +//! +//! This module provides dependency analysis for top-level definitions to enable +//! proper evaluation ordering. It computes Strongly Connected Components (SCCs) +//! using Tarjan's algorithm and provides a topologically sorted evaluation order. +//! +//! NOTE: This handles ALL top-level definitions including: +//! - Regular top-level definitions (e.g., `foo = 42`) +//! - Associated items (e.g., `TypeName.item_name = 5` from `TypeName := T.{ item_name = 5 }`) +//! +//! Associated items are definitions nested under nominal type declarations and have +//! qualified names. They are stored in `all_defs` alongside regular top-level defs. + +const std = @import("std"); +const base = @import("base"); +const CIR = @import("CIR.zig"); +const ModuleEnv = @import("ModuleEnv.zig"); + +/// Represents a directed graph of dependencies between top-level definitions. +/// Edges point from dependent to dependency (A -> B means A depends on B). +pub const DependencyGraph = struct { + /// Map from def_idx to list of def_idx it depends on + edges: std.AutoHashMapUnmanaged(CIR.Def.Idx, std.ArrayList(CIR.Def.Idx)), + + /// All defs in the graph + nodes: []const CIR.Def.Idx, + + allocator: std.mem.Allocator, + + pub fn init(allocator: std.mem.Allocator, defs: []const CIR.Def.Idx) DependencyGraph { + return DependencyGraph{ + .edges = .{}, + .nodes = defs, + .allocator = allocator, + }; + } + + pub fn deinit(self: *DependencyGraph) void { + var iter = self.edges.valueIterator(); + while (iter.next()) |list| { + list.deinit(self.allocator); + } + self.edges.deinit(self.allocator); + } + + /// Add an edge: from_def depends on to_def + pub fn addEdge(self: *DependencyGraph, from_def: CIR.Def.Idx, to_def: CIR.Def.Idx) std.mem.Allocator.Error!void { + const gop = try self.edges.getOrPut(self.allocator, from_def); + if (!gop.found_existing) { + gop.value_ptr.* = .{}; + } + try gop.value_ptr.append(self.allocator, to_def); + } + + /// Get dependencies of a def + pub fn getDependencies(self: *const DependencyGraph, def: CIR.Def.Idx) []const CIR.Def.Idx { + const list = self.edges.get(def) orelse return &.{}; + return list.items; + } +}; + +/// A Strongly Connected Component (SCC) in the dependency graph. +/// Contains one or more definitions that may be mutually recursive. +pub const SCC = struct { + /// Definitions in this SCC + defs: []CIR.Def.Idx, + + /// True if this SCC contains recursion (size > 1 or has self-loop) + is_recursive: bool, + + pub const Idx = enum(u32) { _ }; +}; + +/// The computed evaluation order for all definitions in a module. +/// SCCs are arranged in topological order (dependencies come before dependents). +pub const EvaluationOrder = struct { + /// SCCs in topologically sorted order + /// (dependencies come before dependents) + sccs: []SCC, + + allocator: std.mem.Allocator, + + pub fn deinit(self: *EvaluationOrder) void { + for (self.sccs) |scc| { + self.allocator.free(scc.defs); + } + self.allocator.free(self.sccs); + } +}; + +/// Collects all definition dependencies from an expression +/// Returns a list of Ident.Idx that this expression references +fn collectExprDependencies( + cir: *const ModuleEnv, + expr_idx: CIR.Expr.Idx, + dependencies: *std.AutoHashMapUnmanaged(base.Ident.Idx, void), + allocator: std.mem.Allocator, +) std.mem.Allocator.Error!void { + const expr = cir.store.getExpr(expr_idx); + + switch (expr) { + .e_lookup_local => |lookup| { + // This is a variable reference - add to dependencies + const pattern = cir.store.getPattern(lookup.pattern_idx); + if (pattern == .assign) { + try dependencies.put(allocator, pattern.assign.ident, {}); + } + }, + + .e_call => |call| { + // Recurse into function and arguments + try collectExprDependencies(cir, call.func, dependencies, allocator); + for (cir.store.sliceExpr(call.args)) |arg_idx| { + try collectExprDependencies(cir, arg_idx, dependencies, allocator); + } + }, + + .e_lambda => |lambda| { + // Recurse into lambda body + // Note: Lambda parameters are collected here but filtered out later in buildDependencyGraph() + // when converting idents to def indices (they won't be in the ident_to_def map) + try collectExprDependencies(cir, lambda.body, dependencies, allocator); + }, + + .e_closure => |closure| { + // Recurse into the lambda expression + try collectExprDependencies(cir, closure.lambda_idx, dependencies, allocator); + }, + + .e_if => |if_expr| { + for (cir.store.sliceIfBranches(if_expr.branches)) |branch_idx| { + const branch = cir.store.getIfBranch(branch_idx); + try collectExprDependencies(cir, branch.cond, dependencies, allocator); + try collectExprDependencies(cir, branch.body, dependencies, allocator); + } + try collectExprDependencies(cir, if_expr.final_else, dependencies, allocator); + }, + + .e_match => |match_expr| { + try collectExprDependencies(cir, match_expr.cond, dependencies, allocator); + for (cir.store.sliceMatchBranches(match_expr.branches)) |branch_idx| { + const branch = cir.store.getMatchBranch(branch_idx); + try collectExprDependencies(cir, branch.value, dependencies, allocator); + if (branch.guard) |guard_idx| { + try collectExprDependencies(cir, guard_idx, dependencies, allocator); + } + } + }, + + .e_list => |list| { + for (cir.store.sliceExpr(list.elems)) |elem_idx| { + try collectExprDependencies(cir, elem_idx, dependencies, allocator); + } + }, + + .e_record => |record| { + for (cir.store.sliceRecordFields(record.fields)) |field_idx| { + const field = cir.store.getRecordField(field_idx); + try collectExprDependencies(cir, field.value, dependencies, allocator); + } + // Handle record update syntax: { ..base, field: value } + if (record.ext) |ext_idx| { + try collectExprDependencies(cir, ext_idx, dependencies, allocator); + } + }, + + .e_dot_access => |access| { + try collectExprDependencies(cir, access.receiver, dependencies, allocator); + if (access.args) |args_span| { + for (cir.store.sliceExpr(args_span)) |arg_idx| { + try collectExprDependencies(cir, arg_idx, dependencies, allocator); + } + } + }, + + .e_tuple => |tuple| { + for (cir.store.sliceExpr(tuple.elems)) |elem_idx| { + try collectExprDependencies(cir, elem_idx, dependencies, allocator); + } + }, + + .e_binop => |binop| { + try collectExprDependencies(cir, binop.lhs, dependencies, allocator); + try collectExprDependencies(cir, binop.rhs, dependencies, allocator); + }, + + .e_unary_minus => |unop| { + try collectExprDependencies(cir, unop.expr, dependencies, allocator); + }, + + .e_unary_not => |unop| { + try collectExprDependencies(cir, unop.expr, dependencies, allocator); + }, + + .e_block => |block| { + // Recurse into the block's statements + for (cir.store.sliceStatements(block.stmts)) |stmt_idx| { + const stmt = cir.store.getStatement(stmt_idx); + switch (stmt) { + .s_decl => |decl| { + try collectExprDependencies(cir, decl.expr, dependencies, allocator); + }, + .s_decl_gen => |decl| { + try collectExprDependencies(cir, decl.expr, dependencies, allocator); + }, + .s_var => |var_stmt| { + try collectExprDependencies(cir, var_stmt.expr, dependencies, allocator); + }, + .s_reassign => |reassign| { + try collectExprDependencies(cir, reassign.expr, dependencies, allocator); + }, + .s_dbg => |dbg| { + try collectExprDependencies(cir, dbg.expr, dependencies, allocator); + }, + .s_expr => |expr_stmt| { + try collectExprDependencies(cir, expr_stmt.expr, dependencies, allocator); + }, + .s_expect => |expect| { + try collectExprDependencies(cir, expect.body, dependencies, allocator); + }, + .s_for => |for_stmt| { + try collectExprDependencies(cir, for_stmt.expr, dependencies, allocator); + }, + .s_while => |while_stmt| { + try collectExprDependencies(cir, while_stmt.cond, dependencies, allocator); + try collectExprDependencies(cir, while_stmt.body, dependencies, allocator); + }, + .s_return => |ret| { + try collectExprDependencies(cir, ret.expr, dependencies, allocator); + }, + .s_import, .s_alias_decl, .s_nominal_decl, .s_type_anno, .s_type_var_alias, .s_crash, .s_runtime_error => {}, + } + } + // Recurse into the final expression + try collectExprDependencies(cir, block.final_expr, dependencies, allocator); + }, + + .e_tag => |tag| { + for (cir.store.sliceExpr(tag.args)) |arg_idx| { + try collectExprDependencies(cir, arg_idx, dependencies, allocator); + } + }, + + .e_nominal => |nominal| { + try collectExprDependencies(cir, nominal.backing_expr, dependencies, allocator); + }, + + // Literals and hosted lambdas have no dependencies + .e_num, .e_frac_f32, .e_frac_f64, .e_dec, .e_dec_small, .e_str, .e_str_segment, .e_empty_list, .e_empty_record, .e_zero_argument_tag, .e_ellipsis, .e_anno_only, .e_hosted_lambda => {}, + + .e_low_level_lambda => |ll| { + try collectExprDependencies(cir, ll.body, dependencies, allocator); + }, + + // External lookups reference other modules - skip for now + .e_lookup_external => {}, + + // Required lookups reference app-provided values - skip for dependency analysis + .e_lookup_required => {}, + + .e_nominal_external => |nominal| { + try collectExprDependencies(cir, nominal.backing_expr, dependencies, allocator); + }, + + // Crash has a string literal message (no dependencies) + .e_crash => {}, + + .e_dbg => |dbg| { + try collectExprDependencies(cir, dbg.expr, dependencies, allocator); + }, + + .e_expect => |expect| { + try collectExprDependencies(cir, expect.body, dependencies, allocator); + }, + + .e_return => |ret| { + try collectExprDependencies(cir, ret.expr, dependencies, allocator); + }, + + .e_for => |for_expr| { + try collectExprDependencies(cir, for_expr.expr, dependencies, allocator); + try collectExprDependencies(cir, for_expr.body, dependencies, allocator); + }, + + .e_type_var_dispatch => |tvd| { + // Collect dependencies from the arguments + for (cir.store.exprSlice(tvd.args)) |arg_idx| { + try collectExprDependencies(cir, arg_idx, dependencies, allocator); + } + }, + + .e_runtime_error => {}, + } +} + +/// Build a dependency graph for all definitions +pub fn buildDependencyGraph( + cir: *const ModuleEnv, + all_defs: CIR.Def.Span, + allocator: std.mem.Allocator, +) std.mem.Allocator.Error!DependencyGraph { + const defs_slice = cir.store.sliceDefs(all_defs); + var graph = DependencyGraph.init(allocator, defs_slice); + errdefer graph.deinit(); + + // Map from Ident.Idx to Def.Idx for resolving references + var ident_to_def = std.AutoHashMapUnmanaged(base.Ident.Idx, CIR.Def.Idx){}; + defer ident_to_def.deinit(allocator); + + // First pass: build ident -> def mapping + for (defs_slice) |def_idx| { + const def = cir.store.getDef(def_idx); + const pattern = cir.store.getPattern(def.pattern); + + if (pattern == .assign) { + try ident_to_def.put(allocator, pattern.assign.ident, def_idx); + } + } + + // Second pass: collect dependencies and build graph + for (defs_slice) |def_idx| { + const def = cir.store.getDef(def_idx); + + // Collect all identifiers this def's expression references + var deps = std.AutoHashMapUnmanaged(base.Ident.Idx, void){}; + defer deps.deinit(allocator); + + try collectExprDependencies(cir, def.expr, &deps, allocator); + + // Convert ident dependencies to def dependencies + var dep_iter = deps.keyIterator(); + while (dep_iter.next()) |ident_idx| { + if (ident_to_def.get(ident_idx.*)) |dep_def_idx| { + try graph.addEdge(def_idx, dep_def_idx); + } + // If ident not found in ident_to_def, it's either: + // - A builtin function + // - An external module reference + // - A parameter/local variable + // In all cases, we don't need to track it for top-level evaluation order + } + } + + return graph; +} + +/// Tarjan's algorithm for finding strongly connected components +pub fn computeSCCs( + graph: *const DependencyGraph, + allocator: std.mem.Allocator, +) std.mem.Allocator.Error!EvaluationOrder { + var state = TarjanState.init(allocator); + defer state.deinit(); + + // Run DFS from each unvisited node + for (graph.nodes) |node| { + if (!state.visited.contains(node)) { + try state.strongConnect(graph, node); + } + } + + // Note: state.sccs is already in topological order (dependencies before dependents) + // because Tarjan's algorithm adds SCCs in post-order of DFS traversal. + // When we follow edges from A to B (A depends on B), B finishes first, + // so B's SCC is added before A's SCC. + + return EvaluationOrder{ + .sccs = try state.sccs.toOwnedSlice(allocator), + .allocator = allocator, + }; +} + +const TarjanState = struct { + /// Current DFS index + index: u32, + + /// Map from node to its DFS index + indices: std.AutoHashMapUnmanaged(CIR.Def.Idx, u32), + + /// Map from node to its lowlink value + lowlinks: std.AutoHashMapUnmanaged(CIR.Def.Idx, u32), + + /// Set of visited nodes + visited: std.AutoHashMapUnmanaged(CIR.Def.Idx, void), + + /// Stack for Tarjan's algorithm + stack: std.ArrayList(CIR.Def.Idx), + + /// Set of nodes currently on stack + on_stack: std.AutoHashMapUnmanaged(CIR.Def.Idx, void), + + /// Resulting SCCs (in reverse topological order during construction) + sccs: std.ArrayList(SCC), + + allocator: std.mem.Allocator, + + fn init(allocator: std.mem.Allocator) TarjanState { + return .{ + .index = 0, + .indices = .{}, + .lowlinks = .{}, + .visited = .{}, + .stack = .{}, + .on_stack = .{}, + .sccs = .{}, + .allocator = allocator, + }; + } + + fn deinit(self: *TarjanState) void { + self.indices.deinit(self.allocator); + self.lowlinks.deinit(self.allocator); + self.visited.deinit(self.allocator); + self.stack.deinit(self.allocator); + self.on_stack.deinit(self.allocator); + // Note: sccs ownership transferred to EvaluationOrder, don't free here + self.sccs.deinit(self.allocator); + } + + fn strongConnect( + self: *TarjanState, + graph: *const DependencyGraph, + v: CIR.Def.Idx, + ) std.mem.Allocator.Error!void { + // Set the depth index for v + try self.indices.put(self.allocator, v, self.index); + try self.lowlinks.put(self.allocator, v, self.index); + try self.visited.put(self.allocator, v, {}); + self.index += 1; + + try self.stack.append(self.allocator, v); + try self.on_stack.put(self.allocator, v, {}); + + // Consider successors of v + const dependencies = graph.getDependencies(v); + for (dependencies) |w| { + if (!self.visited.contains(w)) { + // Successor w has not yet been visited; recurse on it + try self.strongConnect(graph, w); + const v_lowlink = self.lowlinks.get(v).?; + const w_lowlink = self.lowlinks.get(w).?; + try self.lowlinks.put(self.allocator, v, @min(v_lowlink, w_lowlink)); + } else if (self.on_stack.contains(w)) { + // Successor w is on stack, hence in the current SCC + const v_lowlink = self.lowlinks.get(v).?; + const w_index = self.indices.get(w).?; + try self.lowlinks.put(self.allocator, v, @min(v_lowlink, w_index)); + } + } + + // If v is a root node, pop the stack and create an SCC + const v_lowlink = self.lowlinks.get(v).?; + const v_index = self.indices.get(v).?; + if (v_lowlink == v_index) { + var scc_defs = std.ArrayList(CIR.Def.Idx){}; + + while (true) { + const w = self.stack.pop() orelse unreachable; // Stack should not be empty + _ = self.on_stack.remove(w); + try scc_defs.append(self.allocator, w); + + if (@intFromEnum(w) == @intFromEnum(v)) break; + } + + // Check if this SCC is recursive + const is_recursive = scc_defs.items.len > 1 or blk: { + // Check for self-loop + if (scc_defs.items.len == 1) { + const node = scc_defs.items[0]; + const deps = graph.getDependencies(node); + for (deps) |dep| { + if (@intFromEnum(dep) == @intFromEnum(node)) break :blk true; + } + } + break :blk false; + }; + + try self.sccs.append(self.allocator, .{ + .defs = try scc_defs.toOwnedSlice(self.allocator), + .is_recursive = is_recursive, + }); + } + } +}; diff --git a/src/canonicalize/Diagnostic.zig b/src/canonicalize/Diagnostic.zig index e90b18d07f..d6b65fbb6e 100644 --- a/src/canonicalize/Diagnostic.zig +++ b/src/canonicalize/Diagnostic.zig @@ -30,9 +30,6 @@ pub const Diagnostic = union(enum) { invalid_num_literal: struct { region: Region, }, - invalid_single_quote: struct { - region: Region, - }, empty_tuple: struct { region: Region, }, @@ -44,6 +41,10 @@ pub const Diagnostic = union(enum) { ident: Ident.Idx, region: Region, }, + qualified_ident_does_not_exist: struct { + ident: Ident.Idx, // The full qualified identifier (e.g., "Stdout.line!") + region: Region, + }, invalid_top_level_statement: struct { stmt: StringLiteral.Idx, region: Region, @@ -75,6 +76,9 @@ pub const Diagnostic = union(enum) { if_else_not_canonicalized: struct { region: Region, }, + if_expr_without_else: struct { + region: Region, + }, malformed_type_annotation: struct { region: Region, }, @@ -114,10 +118,25 @@ pub const Diagnostic = union(enum) { type_name: Ident.Idx, region: Region, }, + type_from_missing_module: struct { + module_name: Ident.Idx, + type_name: Ident.Idx, + region: Region, + }, module_not_imported: struct { module_name: Ident.Idx, region: Region, }, + nested_type_not_found: struct { + parent_name: Ident.Idx, + nested_name: Ident.Idx, + region: Region, + }, + nested_value_not_found: struct { + parent_name: Ident.Idx, + nested_name: Ident.Idx, + region: Region, + }, too_many_exports: struct { count: u32, region: Region, @@ -137,6 +156,43 @@ pub const Diagnostic = union(enum) { crash_expects_string: struct { region: Region, }, + type_module_missing_matching_type: struct { + module_name: Ident.Idx, + region: Region, + }, + default_app_missing_main: struct { + module_name: Ident.Idx, + region: Region, + }, + default_app_wrong_arity: struct { + arity: u32, + region: Region, + }, + cannot_import_default_app: struct { + module_name: Ident.Idx, + region: Region, + }, + execution_requires_app_or_default_app: struct { + region: Region, + }, + type_name_case_mismatch: struct { + module_name: Ident.Idx, + type_name: Ident.Idx, + region: Region, + }, + module_header_deprecated: struct { + region: Region, + }, + redundant_expose_main_type: struct { + type_name: Ident.Idx, + module_name: Ident.Idx, + region: Region, + }, + invalid_main_type_rename_in_exposing: struct { + type_name: Ident.Idx, + alias: Ident.Idx, + region: Region, + }, type_alias_redeclared: struct { name: Ident.Idx, original_region: Region, @@ -189,14 +245,14 @@ pub const Diagnostic = union(enum) { suggested_name: Ident.Idx, region: Region, }, - type_var_ending_in_underscore: struct { + type_var_starting_with_dollar: struct { name: Ident.Idx, suggested_name: Ident.Idx, region: Region, }, pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: base.DataSpan }; + pub const Span = extern struct { span: base.DataSpan }; /// Helper to extract the region from any diagnostic variant pub fn toRegion(self: Diagnostic) Region { @@ -207,6 +263,7 @@ pub const Diagnostic = union(enum) { .invalid_num_literal => |d| d.region, .ident_already_in_scope => |d| d.region, .ident_not_in_scope => |d| d.region, + .qualified_ident_does_not_exist => |d| d.region, .invalid_top_level_statement => |d| d.region, .expr_not_canonicalized => |d| d.region, .invalid_string_interpolation => |d| d.region, @@ -227,12 +284,24 @@ pub const Diagnostic = union(enum) { .module_not_found => |d| d.region, .value_not_exposed => |d| d.region, .type_not_exposed => |d| d.region, + .type_from_missing_module => |d| d.region, .module_not_imported => |d| d.region, + .nested_type_not_found => |d| d.region, + .nested_value_not_found => |d| d.region, .too_many_exports => |d| d.region, .undeclared_type => |d| d.region, .undeclared_type_var => |d| d.region, .type_alias_but_needed_nominal => |d| d.region, .crash_expects_string => |d| d.region, + .type_module_missing_matching_type => |d| d.region, + .default_app_missing_main => |d| d.region, + .default_app_wrong_arity => |d| d.region, + .cannot_import_default_app => |d| d.region, + .execution_requires_app_or_default_app => |d| d.region, + .type_name_case_mismatch => |d| d.region, + .module_header_deprecated => |d| d.region, + .redundant_expose_main_type => |d| d.region, + .invalid_main_type_rename_in_exposing => |d| d.region, .type_alias_redeclared => |d| d.redeclared_region, .nominal_type_redeclared => |d| d.redeclared_region, .type_shadowed_warning => |d| d.region, @@ -245,7 +314,7 @@ pub const Diagnostic = union(enum) { .f64_pattern_literal => |d| d.region, .unused_type_var_name => |d| d.region, .type_var_marked_unused => |d| d.region, - .type_var_ending_in_underscore => |d| d.region, + .type_var_starting_with_dollar => |d| d.region, .underscore_in_type_declaration => |d| d.region, }; } @@ -290,7 +359,7 @@ pub const Diagnostic = union(enum) { } /// Build a report for "invalid number literal" diagnostic - pub fn buildInvalidNumLiteralReport( + pub fn buildInvalidNumeralReport( allocator: Allocator, region_info: base.RegionInfo, literal_text: []const u8, @@ -399,7 +468,6 @@ pub const Diagnostic = union(enum) { allocator: Allocator, ident_name: []const u8, region_info: base.RegionInfo, - original_region_info: base.RegionInfo, filename: []const u8, source: []const u8, line_starts: []const u32, @@ -421,10 +489,6 @@ pub const Diagnostic = union(enum) { line_starts, ); - // we don't need to display the original region info - // as this header is in a single location - _ = original_region_info; - try report.document.addReflowingText("You can remove the duplicate entry to fix this warning."); return report; @@ -450,6 +514,41 @@ pub const Diagnostic = union(enum) { try report.document.addReflowingText(" or "); try report.document.addKeyword("exposing"); try report.document.addReflowingText(" missing up-top?"); + + // Check for common misspellings and add a tip if found + if (reporting.CommonMisspellings.getIdentifierTip(ident_name)) |tip| { + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Tip: "); + try report.document.addReflowingTextWithBackticks(tip); + } + + try report.document.addLineBreak(); + try report.document.addLineBreak(); + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + source, + line_starts, + ); + return report; + } + + /// Build a report for "qualified ident does not exist" diagnostic + pub fn buildQualifiedIdentDoesNotExistReport( + allocator: Allocator, + ident_name: []const u8, + region_info: base.RegionInfo, + filename: []const u8, + source: []const u8, + line_starts: []const u32, + ) !Report { + var report = Report.init(allocator, "DOES NOT EXIST", .runtime_error); + const owned_ident = try report.addOwnedString(ident_name); + try report.document.addUnqualifiedSymbol(owned_ident); + try report.document.addReflowingText(" does not exist."); try report.document.addLineBreak(); try report.document.addLineBreak(); const owned_filename = try report.addOwnedString(filename); @@ -812,9 +911,19 @@ pub const Diagnostic = union(enum) { ) !Report { var report = Report.init(allocator, "UNDECLARED TYPE", .runtime_error); const owned_type_name = try report.addOwnedString(type_name); - try report.document.addReflowingText("The type "); - try report.document.addType(owned_type_name); - try report.document.addReflowingText(" is not declared in this scope."); + + // Check if this looks like a qualified type (contains dots) + const has_dots = std.mem.indexOfScalar(u8, type_name, '.') != null; + + if (has_dots) { + try report.document.addReflowingText("Cannot resolve qualified type "); + try report.document.addType(owned_type_name); + try report.document.addReflowingText("."); + } else { + try report.document.addReflowingText("The type "); + try report.document.addType(owned_type_name); + try report.document.addReflowingText(" is not declared in this scope."); + } try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -1334,13 +1443,32 @@ pub const Diagnostic = union(enum) { const owned_module = try report.addOwnedString(module_name); const owned_type = try report.addOwnedString(type_name); - try report.document.addReflowingText("The "); - try report.document.addModuleName(owned_module); - try report.document.addReflowingText(" module does not expose anything named "); - try report.document.addType(owned_type); - try report.document.addReflowingText("."); - try report.document.addLineBreak(); - try report.document.addReflowingText("Make sure the module exports this type, or use a type that is exposed."); + + // Check if trying to access a type with the same name as the module (e.g., Try.Try) + const is_same_name = std.mem.eql(u8, module_name, type_name); + + if (is_same_name) { + // Special message for Try.Try, Color.Color, etc. + const qualified_name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ module_name, type_name }); + defer allocator.free(qualified_name); + const owned_qualified = try report.addOwnedString(qualified_name); + + try report.document.addReflowingText("There is no "); + try report.document.addType(owned_qualified); + try report.document.addReflowingText(" type."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + } else { + // Standard message for other cases (e.g., Color.RGB where Color is a nominal type) + const qualified_name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ module_name, type_name }); + defer allocator.free(qualified_name); + const owned_qualified = try report.addOwnedString(qualified_name); + + try report.document.addType(owned_qualified); + try report.document.addReflowingText(" does not exist."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + } const owned_filename = try report.addOwnedString(filename); try report.document.addSourceRegion( @@ -1351,6 +1479,21 @@ pub const Diagnostic = union(enum) { line_starts, ); + // Add tip at the end + try report.document.addLineBreak(); + if (is_same_name) { + try report.document.addReflowingText("There is a "); + try report.document.addModuleName(owned_module); + try report.document.addReflowingText(" module, but it does not have a "); + try report.document.addType(owned_type); + try report.document.addReflowingText(" type nested inside it."); + } else { + try report.document.addType(owned_module); + try report.document.addReflowingText(" is a valid type, but it does not have an associated "); + try report.document.addType(owned_type); + try report.document.addReflowingText("."); + } + return report; } diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index 383118a4db..1499e501fe 100644 --- a/src/canonicalize/Expression.zig +++ b/src/canonicalize/Expression.zig @@ -43,21 +43,25 @@ const Self = Expr; /// An expression in the Roc language. pub const Expr = union(enum) { - /// An integer literal with a specific value. + /// An number literal with a specific value. /// Represents whole numbers in various bases (decimal, hex, octal, binary). /// /// ```roc - /// 42 # Decimal integer + /// 42 # Decimal number /// 0xFF # Hexadecimal integer /// 0o755 # Octal integer /// 0b1010 # Binary integer + /// 42u8 # Decimal number with type suffix + /// 42f32 # Decimal number with type suffix /// ``` - e_int: struct { + e_num: struct { value: CIR.IntValue, + kind: CIR.NumKind, }, /// A 32-bit floating-point literal. e_frac_f32: struct { value: f32, + has_suffix: bool, // If the value had a `f32` suffix }, /// A 64-bit floating-point literal. /// Used for approximate decimal representations when F64 type is explicitly required for increased performance. @@ -68,6 +72,7 @@ pub const Expr = union(enum) { /// ``` e_frac_f64: struct { value: f64, + has_suffix: bool, // If the value had a `f64` suffix }, /// A high-precision decimal literal. /// Used for exact decimal arithmetic without floating-point precision issues. @@ -77,8 +82,9 @@ pub const Expr = union(enum) { /// 3.14159265358979323846 # High precision decimal /// 0.1 + 0.2 # Equals exactly 0.3 (not 0.30000000000000004) /// ``` - e_frac_dec: struct { + e_dec: struct { value: RocDec, + has_suffix: bool, // If the value had a `dec` suffix }, /// A small decimal literal stored as a rational number (numerator/10^denominator). /// Memory-efficient representation for common decimal values. @@ -90,8 +96,8 @@ pub const Expr = union(enum) { /// 42.0 # Stored as numerator=420, denominator_power_of_ten=1 (420/10) /// ``` e_dec_small: struct { - numerator: i16, - denominator_power_of_ten: u8, + value: CIR.SmallDecValue, + has_suffix: bool, // If the value had a `dec` suffix }, // A single segment of a string literal // a single string may be made up of a span sequential segments @@ -122,12 +128,23 @@ pub const Expr = union(enum) { target_node_idx: u16, region: Region, }, + /// Lookup of a required identifier from the platform's `requires` clause. + /// This represents a value that the app provides to the platform. + /// ```roc + /// platform "..." + /// requires { main! : () => {} } + /// ... + /// main_for_host! = main! # "main!" here is a required lookup + /// ``` + e_lookup_required: struct { + /// Index into env.requires_types for this required identifier + requires_idx: ModuleEnv.RequiredType.SafeList.Idx, + }, /// A sequence of zero or more elements of the same type /// ```roc /// ["one", "two", "three"] /// ``` e_list: struct { - elem_var: TypeVar, elems: Expr.Span, }, /// Empty list constant `[]` @@ -162,6 +179,7 @@ pub const Expr = union(enum) { /// This is *only* for calling functions, not for tag application. /// The Tag variant contains any applied values inside it. e_call: struct { + func: Expr.Idx, args: Expr.Span, called_via: CalledVia, }, @@ -214,7 +232,7 @@ pub const Expr = union(enum) { /// A qualified, nominal type /// /// ```roc - /// Result.Ok("success") # Tags + /// Try.Ok("success") # Tags /// Config.{ optimize : Bool} # Records /// Point.(1.0, 2.0) # Tuples /// Point.(1.0) # Values @@ -227,7 +245,7 @@ pub const Expr = union(enum) { /// An external qualified, nominal type /// /// ```roc - /// OtherModule.Result.Ok("success") # Tags + /// OtherModule.Try.Ok("success") # Tags /// OtherModule.Config.{ optimize : Bool} # Records /// OtherModule.Point.(1.0, 2.0) # Tuples /// OtherModule.Point.(1.0) # Values @@ -297,6 +315,7 @@ pub const Expr = union(enum) { e_dot_access: struct { receiver: Expr.Idx, // Expression before the dot (e.g., `list` in `list.map`) field_name: Ident.Idx, // Identifier after the dot (e.g., `map` in `list.map`) + field_name_region: base.Region, // Region of just the field/method name for error reporting args: ?Expr.Span, // Optional arguments for method calls (e.g., `fn` in `list.map(fn)`) }, /// Runtime error expression that crashes when executed. @@ -351,9 +370,792 @@ pub const Expr = union(enum) { /// launchTheNukes: |{}| ... /// ``` e_ellipsis: struct {}, + /// A standalone type annotation without a body. + /// This represents a type declaration that has no implementation. + /// During type-checking, this expression is assigned the type from its annotation. + /// + /// ```roc + /// foo : {} -> {} + /// ``` + e_anno_only: struct {}, + + /// Early return expression that exits the enclosing function with a value. + /// This is used when `return` appears as the final expression in a block. + /// Unlike a normal expression, evaluating this causes the function to return + /// immediately with the contained value. + /// + /// ```roc + /// if condition { + /// return value # Early return from enclosing function + /// } + /// ``` + e_return: struct { + expr: Expr.Idx, + }, + + /// Type variable dispatch expression for calling methods on type variable aliases. + /// This is created when the user writes `Thing.method(args)` inside a function body + /// where `Thing` is a type variable alias introduced by a statement like `Thing : thing`. + /// + /// The actual function to call is resolved during type-checking once the type variable + /// is unified with a concrete type. For example, if `thing` resolves to `List(a)`, + /// then `Thing.len(x)` becomes `List.len(x)`. + /// + /// ```roc + /// default_value : |thing| thing where thing implements Default + /// default_value = |thing| + /// Thing : thing + /// Thing.default() # Calls List.default, Bool.default, etc. based on concrete type + /// ``` + e_type_var_dispatch: struct { + /// Reference to the s_type_var_alias statement that introduced this type alias + type_var_alias_stmt: CIR.Statement.Idx, + /// The method name being called (e.g., "default" in Thing.default()) + method_name: Ident.Idx, + /// Arguments to the method call (may be empty for no-arg methods) + args: Expr.Span, + }, + + /// For expression that iterates over a list and executes a body for each element. + /// The for expression evaluates to the empty record `{}`. + /// This is the expression form of a for loop, allowing it to be used in expression contexts. + /// + /// ```roc + /// for_each! = |items, cb!| for item in items { cb!(item) } + /// ``` + e_for: struct { + patt: CIR.Pattern.Idx, + expr: Expr.Idx, + body: Expr.Idx, + }, + + /// A hosted function that will be provided by the platform at runtime. + /// This represents a lambda/function whose implementation is provided by the host application + /// via the RocOps.hosted_fns array. + /// + /// ```roc + /// # Stdout.line! is a hosted function provided by the platform + /// line! : Str => {} + /// ``` + e_hosted_lambda: struct { + symbol_name: base.Ident.Idx, + index: u32, // Index into RocOps.hosted_fns (assigned during canonicalization) + args: CIR.Pattern.Span, + body: Expr.Idx, + }, + + /// A low-level builtin operation. + /// This represents a lambda/function that will be implemented by the compiler backend. + /// Like e_anno_only, it has no Roc implementation, but unlike e_anno_only, + /// it's expected to be implemented by the backend rather than being an error. + /// It behaves like e_lambda in that it has parameters and a body (which crashes when evaluated). + /// + /// ```roc + /// # Str.is_empty is a low-level operation + /// is_empty : Str -> Bool + /// ``` + e_low_level_lambda: struct { + op: LowLevel, + args: CIR.Pattern.Span, + body: Expr.Idx, + }, + + /// Low-level builtin operations that are implemented by the compiler backend. + pub const LowLevel = enum { + // String operations + str_is_empty, + str_is_eq, + str_concat, + str_contains, + str_trim, + str_trim_start, + str_trim_end, + str_caseless_ascii_equals, + str_with_ascii_lowercased, + str_with_ascii_uppercased, + str_starts_with, + str_ends_with, + str_repeat, + str_with_prefix, + str_drop_prefix, + str_drop_suffix, + str_count_utf8_bytes, + str_with_capacity, + str_reserve, + str_release_excess_capacity, + str_to_utf8, + str_from_utf8_lossy, + str_from_utf8, + str_split_on, + str_join_with, + str_inspekt, + + // Numeric to_str operations + u8_to_str, + i8_to_str, + u16_to_str, + i16_to_str, + u32_to_str, + i32_to_str, + u64_to_str, + i64_to_str, + u128_to_str, + i128_to_str, + dec_to_str, + f32_to_str, + f64_to_str, + + // List operations + list_len, + list_is_empty, + list_get_unsafe, + list_append_unsafe, + list_concat, + list_with_capacity, + list_sort_with, + list_drop_at, + list_sublist, + list_append, + + // Set operations + // set_is_empty, + + // Bool operations + bool_is_eq, + + // Numeric type checking operations + num_is_zero, // All numeric types + num_is_negative, // Signed types only: I8, I16, I32, I64, I128, Dec, F32, F64 + num_is_positive, // Signed types only: I8, I16, I32, I64, I128, Dec, F32, F64 + + // Numeric comparison operations + num_is_eq, // All integer types + Dec (NOT F32/F64 due to float imprecision) + num_is_gt, // All numeric types + num_is_gte, // All numeric types + num_is_lt, // All numeric types + num_is_lte, // All numeric types + + // Numeric arithmetic operations + num_negate, // Signed types only: I8, I16, I32, I64, I128, Dec, F32, F64 + num_abs, // Signed types only: I8, I16, I32, I64, I128, Dec, F32, F64 + num_abs_diff, // All numeric types (signed returns unsigned counterpart) + num_plus, // All numeric types + num_minus, // All numeric types + num_times, // All numeric types + num_div_by, // All numeric types + num_div_trunc_by, // All numeric types + num_rem_by, // All numeric types + num_mod_by, // Integer types only: U8, I8, U16, I16, U32, I32, U64, I64, U128, I128 + + // Bitwise shift operations (integer types only) + num_shift_left_by, // Int a, U8 -> Int a + num_shift_right_by, // Int a, U8 -> Int a (arithmetic shift for signed, logical for unsigned) + num_shift_right_zf_by, // Int a, U8 -> Int a (zero-fill/logical shift) + + // Numeric parsing operations + num_from_int_digits, // Parse List(U8) -> Try(num, [OutOfRange]) + num_from_dec_digits, // Parse (List(U8), List(U8)) -> Try(num, [OutOfRange]) + num_from_numeral, // Parse Numeral -> Try(num, [InvalidNumeral(Str)]) + num_from_str, // Parse Str -> Try(num, [BadNumStr]) + + // Numeric conversion operations (U8) + u8_to_i8_wrap, // U8 -> I8 (wrapping) + u8_to_i8_try, // U8 -> Try(I8, [OutOfRange]) + u8_to_i16, // U8 -> I16 (safe) + u8_to_i32, // U8 -> I32 (safe) + u8_to_i64, // U8 -> I64 (safe) + u8_to_i128, // U8 -> I128 (safe) + u8_to_u16, // U8 -> U16 (safe) + u8_to_u32, // U8 -> U32 (safe) + u8_to_u64, // U8 -> U64 (safe) + u8_to_u128, // U8 -> U128 (safe) + u8_to_f32, // U8 -> F32 (safe) + u8_to_f64, // U8 -> F64 (safe) + u8_to_dec, // U8 -> Dec (safe) + + // Numeric conversion operations (I8) + i8_to_i16, // I8 -> I16 (safe) + i8_to_i32, // I8 -> I32 (safe) + i8_to_i64, // I8 -> I64 (safe) + i8_to_i128, // I8 -> I128 (safe) + i8_to_u8_wrap, // I8 -> U8 (wrapping) + i8_to_u8_try, // I8 -> Try(U8, [OutOfRange]) + i8_to_u16_wrap, // I8 -> U16 (wrapping) + i8_to_u16_try, // I8 -> Try(U16, [OutOfRange]) + i8_to_u32_wrap, // I8 -> U32 (wrapping) + i8_to_u32_try, // I8 -> Try(U32, [OutOfRange]) + i8_to_u64_wrap, // I8 -> U64 (wrapping) + i8_to_u64_try, // I8 -> Try(U64, [OutOfRange]) + i8_to_u128_wrap, // I8 -> U128 (wrapping) + i8_to_u128_try, // I8 -> Try(U128, [OutOfRange]) + i8_to_f32, // I8 -> F32 (safe) + i8_to_f64, // I8 -> F64 (safe) + i8_to_dec, // I8 -> Dec (safe) + + // Numeric conversion operations (U16) + u16_to_i8_wrap, // U16 -> I8 (wrapping) + u16_to_i8_try, // U16 -> Try(I8, [OutOfRange]) + u16_to_i16_wrap, // U16 -> I16 (wrapping) + u16_to_i16_try, // U16 -> Try(I16, [OutOfRange]) + u16_to_i32, // U16 -> I32 (safe) + u16_to_i64, // U16 -> I64 (safe) + u16_to_i128, // U16 -> I128 (safe) + u16_to_u8_wrap, // U16 -> U8 (wrapping) + u16_to_u8_try, // U16 -> Try(U8, [OutOfRange]) + u16_to_u32, // U16 -> U32 (safe) + u16_to_u64, // U16 -> U64 (safe) + u16_to_u128, // U16 -> U128 (safe) + u16_to_f32, // U16 -> F32 (safe) + u16_to_f64, // U16 -> F64 (safe) + u16_to_dec, // U16 -> Dec (safe) + + // Numeric conversion operations (I16) + i16_to_i8_wrap, // I16 -> I8 (wrapping) + i16_to_i8_try, // I16 -> Try(I8, [OutOfRange]) + i16_to_i32, // I16 -> I32 (safe) + i16_to_i64, // I16 -> I64 (safe) + i16_to_i128, // I16 -> I128 (safe) + i16_to_u8_wrap, // I16 -> U8 (wrapping) + i16_to_u8_try, // I16 -> Try(U8, [OutOfRange]) + i16_to_u16_wrap, // I16 -> U16 (wrapping) + i16_to_u16_try, // I16 -> Try(U16, [OutOfRange]) + i16_to_u32_wrap, // I16 -> U32 (wrapping) + i16_to_u32_try, // I16 -> Try(U32, [OutOfRange]) + i16_to_u64_wrap, // I16 -> U64 (wrapping) + i16_to_u64_try, // I16 -> Try(U64, [OutOfRange]) + i16_to_u128_wrap, // I16 -> U128 (wrapping) + i16_to_u128_try, // I16 -> Try(U128, [OutOfRange]) + i16_to_f32, // I16 -> F32 (safe) + i16_to_f64, // I16 -> F64 (safe) + i16_to_dec, // I16 -> Dec (safe) + + // Numeric conversion operations (U32) + u32_to_i8_wrap, // U32 -> I8 (wrapping) + u32_to_i8_try, // U32 -> Try(I8, [OutOfRange]) + u32_to_i16_wrap, // U32 -> I16 (wrapping) + u32_to_i16_try, // U32 -> Try(I16, [OutOfRange]) + u32_to_i32_wrap, // U32 -> I32 (wrapping) + u32_to_i32_try, // U32 -> Try(I32, [OutOfRange]) + u32_to_i64, // U32 -> I64 (safe) + u32_to_i128, // U32 -> I128 (safe) + u32_to_u8_wrap, // U32 -> U8 (wrapping) + u32_to_u8_try, // U32 -> Try(U8, [OutOfRange]) + u32_to_u16_wrap, // U32 -> U16 (wrapping) + u32_to_u16_try, // U32 -> Try(U16, [OutOfRange]) + u32_to_u64, // U32 -> U64 (safe) + u32_to_u128, // U32 -> U128 (safe) + u32_to_f32, // U32 -> F32 (safe) + u32_to_f64, // U32 -> F64 (safe) + u32_to_dec, // U32 -> Dec (safe) + + // Numeric conversion operations (I32) + i32_to_i8_wrap, // I32 -> I8 (wrapping) + i32_to_i8_try, // I32 -> Try(I8, [OutOfRange]) + i32_to_i16_wrap, // I32 -> I16 (wrapping) + i32_to_i16_try, // I32 -> Try(I16, [OutOfRange]) + i32_to_i64, // I32 -> I64 (safe) + i32_to_i128, // I32 -> I128 (safe) + i32_to_u8_wrap, // I32 -> U8 (wrapping) + i32_to_u8_try, // I32 -> Try(U8, [OutOfRange]) + i32_to_u16_wrap, // I32 -> U16 (wrapping) + i32_to_u16_try, // I32 -> Try(U16, [OutOfRange]) + i32_to_u32_wrap, // I32 -> U32 (wrapping) + i32_to_u32_try, // I32 -> Try(U32, [OutOfRange]) + i32_to_u64_wrap, // I32 -> U64 (wrapping) + i32_to_u64_try, // I32 -> Try(U64, [OutOfRange]) + i32_to_u128_wrap, // I32 -> U128 (wrapping) + i32_to_u128_try, // I32 -> Try(U128, [OutOfRange]) + i32_to_f32, // I32 -> F32 (safe) + i32_to_f64, // I32 -> F64 (safe) + i32_to_dec, // I32 -> Dec (safe) + + // Numeric conversion operations (U64) + u64_to_i8_wrap, // U64 -> I8 (wrapping) + u64_to_i8_try, // U64 -> Try(I8, [OutOfRange]) + u64_to_i16_wrap, // U64 -> I16 (wrapping) + u64_to_i16_try, // U64 -> Try(I16, [OutOfRange]) + u64_to_i32_wrap, // U64 -> I32 (wrapping) + u64_to_i32_try, // U64 -> Try(I32, [OutOfRange]) + u64_to_i64_wrap, // U64 -> I64 (wrapping) + u64_to_i64_try, // U64 -> Try(I64, [OutOfRange]) + u64_to_i128, // U64 -> I128 (safe) + u64_to_u8_wrap, // U64 -> U8 (wrapping) + u64_to_u8_try, // U64 -> Try(U8, [OutOfRange]) + u64_to_u16_wrap, // U64 -> U16 (wrapping) + u64_to_u16_try, // U64 -> Try(U16, [OutOfRange]) + u64_to_u32_wrap, // U64 -> U32 (wrapping) + u64_to_u32_try, // U64 -> Try(U32, [OutOfRange]) + u64_to_u128, // U64 -> U128 (safe) + u64_to_f32, // U64 -> F32 (safe) + u64_to_f64, // U64 -> F64 (safe) + u64_to_dec, // U64 -> Dec (safe) + + // Numeric conversion operations (I64) + i64_to_i8_wrap, // I64 -> I8 (wrapping) + i64_to_i8_try, // I64 -> Try(I8, [OutOfRange]) + i64_to_i16_wrap, // I64 -> I16 (wrapping) + i64_to_i16_try, // I64 -> Try(I16, [OutOfRange]) + i64_to_i32_wrap, // I64 -> I32 (wrapping) + i64_to_i32_try, // I64 -> Try(I32, [OutOfRange]) + i64_to_i128, // I64 -> I128 (safe) + i64_to_u8_wrap, // I64 -> U8 (wrapping) + i64_to_u8_try, // I64 -> Try(U8, [OutOfRange]) + i64_to_u16_wrap, // I64 -> U16 (wrapping) + i64_to_u16_try, // I64 -> Try(U16, [OutOfRange]) + i64_to_u32_wrap, // I64 -> U32 (wrapping) + i64_to_u32_try, // I64 -> Try(U32, [OutOfRange]) + i64_to_u64_wrap, // I64 -> U64 (wrapping) + i64_to_u64_try, // I64 -> Try(U64, [OutOfRange]) + i64_to_u128_wrap, // I64 -> U128 (wrapping) + i64_to_u128_try, // I64 -> Try(U128, [OutOfRange]) + i64_to_f32, // I64 -> F32 (safe) + i64_to_f64, // I64 -> F64 (safe) + i64_to_dec, // I64 -> Dec (safe) + + // Numeric conversion operations (U128) + u128_to_i8_wrap, // U128 -> I8 (wrapping) + u128_to_i8_try, // U128 -> Try(I8, [OutOfRange]) + u128_to_i16_wrap, // U128 -> I16 (wrapping) + u128_to_i16_try, // U128 -> Try(I16, [OutOfRange]) + u128_to_i32_wrap, // U128 -> I32 (wrapping) + u128_to_i32_try, // U128 -> Try(I32, [OutOfRange]) + u128_to_i64_wrap, // U128 -> I64 (wrapping) + u128_to_i64_try, // U128 -> Try(I64, [OutOfRange]) + u128_to_i128_wrap, // U128 -> I128 (wrapping) + u128_to_i128_try, // U128 -> Try(I128, [OutOfRange]) + u128_to_u8_wrap, // U128 -> U8 (wrapping) + u128_to_u8_try, // U128 -> Try(U8, [OutOfRange]) + u128_to_u16_wrap, // U128 -> U16 (wrapping) + u128_to_u16_try, // U128 -> Try(U16, [OutOfRange]) + u128_to_u32_wrap, // U128 -> U32 (wrapping) + u128_to_u32_try, // U128 -> Try(U32, [OutOfRange]) + u128_to_u64_wrap, // U128 -> U64 (wrapping) + u128_to_u64_try, // U128 -> Try(U64, [OutOfRange]) + u128_to_f32, // U128 -> F32 (safe) + u128_to_f64, // U128 -> F64 (safe) + u128_to_dec_try_unsafe, // U128 -> { success: Bool, val: Dec } + + // Numeric conversion operations (I128) + i128_to_i8_wrap, // I128 -> I8 (wrapping) + i128_to_i8_try, // I128 -> Try(I8, [OutOfRange]) + i128_to_i16_wrap, // I128 -> I16 (wrapping) + i128_to_i16_try, // I128 -> Try(I16, [OutOfRange]) + i128_to_i32_wrap, // I128 -> I32 (wrapping) + i128_to_i32_try, // I128 -> Try(I32, [OutOfRange]) + i128_to_i64_wrap, // I128 -> I64 (wrapping) + i128_to_i64_try, // I128 -> Try(I64, [OutOfRange]) + i128_to_u8_wrap, // I128 -> U8 (wrapping) + i128_to_u8_try, // I128 -> Try(U8, [OutOfRange]) + i128_to_u16_wrap, // I128 -> U16 (wrapping) + i128_to_u16_try, // I128 -> Try(U16, [OutOfRange]) + i128_to_u32_wrap, // I128 -> U32 (wrapping) + i128_to_u32_try, // I128 -> Try(U32, [OutOfRange]) + i128_to_u64_wrap, // I128 -> U64 (wrapping) + i128_to_u64_try, // I128 -> Try(U64, [OutOfRange]) + i128_to_u128_wrap, // I128 -> U128 (wrapping) + i128_to_u128_try, // I128 -> Try(U128, [OutOfRange]) + i128_to_f32, // I128 -> F32 (safe) + i128_to_f64, // I128 -> F64 (safe) + i128_to_dec_try_unsafe, // I128 -> { success: Bool, val: Dec } + + // Numeric conversion operations (F32) + f32_to_i8_trunc, // F32 -> I8 (truncating) + f32_to_i8_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: I8 } + f32_to_i16_trunc, // F32 -> I16 (truncating) + f32_to_i16_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: I16 } + f32_to_i32_trunc, // F32 -> I32 (truncating) + f32_to_i32_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: I32 } + f32_to_i64_trunc, // F32 -> I64 (truncating) + f32_to_i64_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: I64 } + f32_to_i128_trunc, // F32 -> I128 (truncating) + f32_to_i128_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: I128 } + f32_to_u8_trunc, // F32 -> U8 (truncating) + f32_to_u8_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: U8 } + f32_to_u16_trunc, // F32 -> U16 (truncating) + f32_to_u16_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: U16 } + f32_to_u32_trunc, // F32 -> U32 (truncating) + f32_to_u32_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: U32 } + f32_to_u64_trunc, // F32 -> U64 (truncating) + f32_to_u64_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: U64 } + f32_to_u128_trunc, // F32 -> U128 (truncating) + f32_to_u128_try_unsafe, // F32 -> { is_int: Bool, in_range: Bool, val: U128 } + f32_to_f64, // F32 -> F64 (safe widening) + + // Numeric conversion operations (F64) + f64_to_i8_trunc, // F64 -> I8 (truncating) + f64_to_i8_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: I8 } + f64_to_i16_trunc, // F64 -> I16 (truncating) + f64_to_i16_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: I16 } + f64_to_i32_trunc, // F64 -> I32 (truncating) + f64_to_i32_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: I32 } + f64_to_i64_trunc, // F64 -> I64 (truncating) + f64_to_i64_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: I64 } + f64_to_i128_trunc, // F64 -> I128 (truncating) + f64_to_i128_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: I128 } + f64_to_u8_trunc, // F64 -> U8 (truncating) + f64_to_u8_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: U8 } + f64_to_u16_trunc, // F64 -> U16 (truncating) + f64_to_u16_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: U16 } + f64_to_u32_trunc, // F64 -> U32 (truncating) + f64_to_u32_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: U32 } + f64_to_u64_trunc, // F64 -> U64 (truncating) + f64_to_u64_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: U64 } + f64_to_u128_trunc, // F64 -> U128 (truncating) + f64_to_u128_try_unsafe, // F64 -> { is_int: Bool, in_range: Bool, val: U128 } + f64_to_f32_wrap, // F64 -> F32 (lossy narrowing) + f64_to_f32_try_unsafe, // F64 -> { success: Bool, val: F32 } + + // Numeric conversion operations (Dec) + dec_to_i8_trunc, // Dec -> I8 (truncating) + dec_to_i8_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: I8 } + dec_to_i16_trunc, // Dec -> I16 (truncating) + dec_to_i16_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: I16 } + dec_to_i32_trunc, // Dec -> I32 (truncating) + dec_to_i32_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: I32 } + dec_to_i64_trunc, // Dec -> I64 (truncating) + dec_to_i64_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: I64 } + dec_to_i128_trunc, // Dec -> I128 (truncating) + dec_to_i128_try_unsafe, // Dec -> { is_int: Bool, val: I128 } - always in range + dec_to_u8_trunc, // Dec -> U8 (truncating) + dec_to_u8_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: U8 } + dec_to_u16_trunc, // Dec -> U16 (truncating) + dec_to_u16_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: U16 } + dec_to_u32_trunc, // Dec -> U32 (truncating) + dec_to_u32_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: U32 } + dec_to_u64_trunc, // Dec -> U64 (truncating) + dec_to_u64_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: U64 } + dec_to_u128_trunc, // Dec -> U128 (truncating) + dec_to_u128_try_unsafe, // Dec -> { is_int: Bool, in_range: Bool, val: U128 } + dec_to_f32_wrap, // Dec -> F32 (lossy narrowing) + dec_to_f32_try_unsafe, // Dec -> { success: Bool, val: F32 } + dec_to_f64, // Dec -> F64 (lossy conversion) + + /// Ownership semantics for each argument of a low-level operation. + /// See src/builtins/OWNERSHIP.md for detailed documentation. + pub const ArgOwnership = enum { + /// Function reads argument without affecting refcount. Caller retains ownership. + /// Interpreter should decref after call. + borrow, + /// Function takes ownership of argument. Caller loses access. + /// Interpreter should NOT decref after call. + consume, + }; + + /// Returns the ownership semantics for each argument of this low-level operation. + /// The returned slice has one entry per argument. + /// + /// Important: DO NOT ADD an else branch to this switch statement + /// we expect a compile error if a new case is added and ownership semantics are not defined here. + pub fn getArgOwnership(self: LowLevel) []const ArgOwnership { + return switch (self) { + // String operations - borrowing (read-only) + .str_is_empty, .str_is_eq, .str_contains, .str_starts_with, .str_ends_with, .str_count_utf8_bytes, .str_caseless_ascii_equals => &.{ .borrow, .borrow }, + + // String operations - consuming (take ownership) + .str_concat => &.{ .consume, .borrow }, // first consumed, second borrowed + .str_trim, .str_trim_start, .str_trim_end => &.{.consume}, + .str_with_ascii_lowercased, .str_with_ascii_uppercased => &.{.consume}, + .str_repeat => &.{ .borrow, .borrow }, // string borrowed, count is value type + .str_with_prefix => &.{ .consume, .borrow }, + .str_with_capacity => &.{.borrow}, // capacity is value type + .str_reserve => &.{ .consume, .borrow }, + .str_release_excess_capacity => &.{.consume}, + .str_join_with => &.{ .consume, .borrow }, // list consumed, separator borrowed + + // String operations - borrowing with seamless slice result (incref internally) + .str_split_on => &.{ .borrow, .borrow }, + .str_to_utf8 => &.{.borrow}, + .str_drop_prefix, .str_drop_suffix => &.{ .borrow, .borrow }, + + // String parsing - list consumed + .str_from_utf8, .str_from_utf8_lossy => &.{.consume}, + + // Str.inspect - borrows the value to render it + .str_inspekt => &.{.borrow}, + + // Numeric to_str - value types (no ownership) + .u8_to_str, .i8_to_str, .u16_to_str, .i16_to_str, .u32_to_str, .i32_to_str, .u64_to_str, .i64_to_str, .u128_to_str, .i128_to_str, .dec_to_str, .f32_to_str, .f64_to_str => &.{.borrow}, + + // List operations - borrowing + .list_len, .list_is_empty, .list_get_unsafe => &.{.borrow}, + + // List operations - consuming + .list_concat => &.{ .consume, .consume }, + .list_with_capacity => &.{.borrow}, // capacity is value type + .list_sort_with => &.{.consume}, + .list_append_unsafe => &.{.consume}, + .list_append => &.{ .consume, .borrow }, // list consumed, element borrowed + .list_drop_at => &.{ .consume, .borrow }, // list consumed, index is value type + .list_sublist => &.{ .consume, .borrow }, // list consumed, {start, len} record is value type + + // Bool operations - value types + .bool_is_eq => &.{ .borrow, .borrow }, + + // Numeric operations - all value types (no heap allocation) + .num_is_zero, .num_is_negative, .num_is_positive, .num_negate, .num_abs => &.{.borrow}, + .num_is_eq, .num_is_gt, .num_is_gte, .num_is_lt, .num_is_lte, .num_plus, .num_minus, .num_times, .num_div_by, .num_div_trunc_by, .num_rem_by, .num_mod_by, .num_abs_diff, .num_shift_left_by, .num_shift_right_by, .num_shift_right_zf_by => &.{ .borrow, .borrow }, + + // Numeric parsing - list borrowed for digits, string borrowed + .num_from_int_digits => &.{.borrow}, + .num_from_dec_digits => &.{ .borrow, .borrow }, + .num_from_numeral => &.{.borrow}, + .num_from_str => &.{.borrow}, + + // All numeric conversions are value types (no heap allocation). + // Explicitly listed to get compile errors when new LowLevel variants are added. + .u8_to_i8_wrap, + .u8_to_i8_try, + .u8_to_i16, + .u8_to_i32, + .u8_to_i64, + .u8_to_i128, + .u8_to_u16, + .u8_to_u32, + .u8_to_u64, + .u8_to_u128, + .u8_to_f32, + .u8_to_f64, + .u8_to_dec, + .i8_to_i16, + .i8_to_i32, + .i8_to_i64, + .i8_to_i128, + .i8_to_u8_wrap, + .i8_to_u8_try, + .i8_to_u16_wrap, + .i8_to_u16_try, + .i8_to_u32_wrap, + .i8_to_u32_try, + .i8_to_u64_wrap, + .i8_to_u64_try, + .i8_to_u128_wrap, + .i8_to_u128_try, + .i8_to_f32, + .i8_to_f64, + .i8_to_dec, + .u16_to_i8_wrap, + .u16_to_i8_try, + .u16_to_i16_wrap, + .u16_to_i16_try, + .u16_to_i32, + .u16_to_i64, + .u16_to_i128, + .u16_to_u8_wrap, + .u16_to_u8_try, + .u16_to_u32, + .u16_to_u64, + .u16_to_u128, + .u16_to_f32, + .u16_to_f64, + .u16_to_dec, + .i16_to_i8_wrap, + .i16_to_i8_try, + .i16_to_i32, + .i16_to_i64, + .i16_to_i128, + .i16_to_u8_wrap, + .i16_to_u8_try, + .i16_to_u16_wrap, + .i16_to_u16_try, + .i16_to_u32_wrap, + .i16_to_u32_try, + .i16_to_u64_wrap, + .i16_to_u64_try, + .i16_to_u128_wrap, + .i16_to_u128_try, + .i16_to_f32, + .i16_to_f64, + .i16_to_dec, + .u32_to_i8_wrap, + .u32_to_i8_try, + .u32_to_i16_wrap, + .u32_to_i16_try, + .u32_to_i32_wrap, + .u32_to_i32_try, + .u32_to_i64, + .u32_to_i128, + .u32_to_u8_wrap, + .u32_to_u8_try, + .u32_to_u16_wrap, + .u32_to_u16_try, + .u32_to_u64, + .u32_to_u128, + .u32_to_f32, + .u32_to_f64, + .u32_to_dec, + .i32_to_i8_wrap, + .i32_to_i8_try, + .i32_to_i16_wrap, + .i32_to_i16_try, + .i32_to_i64, + .i32_to_i128, + .i32_to_u8_wrap, + .i32_to_u8_try, + .i32_to_u16_wrap, + .i32_to_u16_try, + .i32_to_u32_wrap, + .i32_to_u32_try, + .i32_to_u64_wrap, + .i32_to_u64_try, + .i32_to_u128_wrap, + .i32_to_u128_try, + .i32_to_f32, + .i32_to_f64, + .i32_to_dec, + .u64_to_i8_wrap, + .u64_to_i8_try, + .u64_to_i16_wrap, + .u64_to_i16_try, + .u64_to_i32_wrap, + .u64_to_i32_try, + .u64_to_i64_wrap, + .u64_to_i64_try, + .u64_to_i128, + .u64_to_u8_wrap, + .u64_to_u8_try, + .u64_to_u16_wrap, + .u64_to_u16_try, + .u64_to_u32_wrap, + .u64_to_u32_try, + .u64_to_u128, + .u64_to_f32, + .u64_to_f64, + .u64_to_dec, + .i64_to_i8_wrap, + .i64_to_i8_try, + .i64_to_i16_wrap, + .i64_to_i16_try, + .i64_to_i32_wrap, + .i64_to_i32_try, + .i64_to_i128, + .i64_to_u8_wrap, + .i64_to_u8_try, + .i64_to_u16_wrap, + .i64_to_u16_try, + .i64_to_u32_wrap, + .i64_to_u32_try, + .i64_to_u64_wrap, + .i64_to_u64_try, + .i64_to_u128_wrap, + .i64_to_u128_try, + .i64_to_f32, + .i64_to_f64, + .i64_to_dec, + .u128_to_i8_wrap, + .u128_to_i8_try, + .u128_to_i16_wrap, + .u128_to_i16_try, + .u128_to_i32_wrap, + .u128_to_i32_try, + .u128_to_i64_wrap, + .u128_to_i64_try, + .u128_to_i128_wrap, + .u128_to_i128_try, + .u128_to_u8_wrap, + .u128_to_u8_try, + .u128_to_u16_wrap, + .u128_to_u16_try, + .u128_to_u32_wrap, + .u128_to_u32_try, + .u128_to_u64_wrap, + .u128_to_u64_try, + .u128_to_f32, + .u128_to_f64, + .u128_to_dec_try_unsafe, + .i128_to_i8_wrap, + .i128_to_i8_try, + .i128_to_i16_wrap, + .i128_to_i16_try, + .i128_to_i32_wrap, + .i128_to_i32_try, + .i128_to_i64_wrap, + .i128_to_i64_try, + .i128_to_u8_wrap, + .i128_to_u8_try, + .i128_to_u16_wrap, + .i128_to_u16_try, + .i128_to_u32_wrap, + .i128_to_u32_try, + .i128_to_u64_wrap, + .i128_to_u64_try, + .i128_to_u128_wrap, + .i128_to_u128_try, + .i128_to_f32, + .i128_to_f64, + .i128_to_dec_try_unsafe, + .f32_to_i8_trunc, + .f32_to_i8_try_unsafe, + .f32_to_i16_trunc, + .f32_to_i16_try_unsafe, + .f32_to_i32_trunc, + .f32_to_i32_try_unsafe, + .f32_to_i64_trunc, + .f32_to_i64_try_unsafe, + .f32_to_i128_trunc, + .f32_to_i128_try_unsafe, + .f32_to_u8_trunc, + .f32_to_u8_try_unsafe, + .f32_to_u16_trunc, + .f32_to_u16_try_unsafe, + .f32_to_u32_trunc, + .f32_to_u32_try_unsafe, + .f32_to_u64_trunc, + .f32_to_u64_try_unsafe, + .f32_to_u128_trunc, + .f32_to_u128_try_unsafe, + .f32_to_f64, + .f64_to_i8_trunc, + .f64_to_i8_try_unsafe, + .f64_to_i16_trunc, + .f64_to_i16_try_unsafe, + .f64_to_i32_trunc, + .f64_to_i32_try_unsafe, + .f64_to_i64_trunc, + .f64_to_i64_try_unsafe, + .f64_to_i128_trunc, + .f64_to_i128_try_unsafe, + .f64_to_u8_trunc, + .f64_to_u8_try_unsafe, + .f64_to_u16_trunc, + .f64_to_u16_try_unsafe, + .f64_to_u32_trunc, + .f64_to_u32_try_unsafe, + .f64_to_u64_trunc, + .f64_to_u64_try_unsafe, + .f64_to_u128_trunc, + .f64_to_u128_try_unsafe, + .f64_to_f32_wrap, + .f64_to_f32_try_unsafe, + .dec_to_i8_trunc, + .dec_to_i8_try_unsafe, + .dec_to_i16_trunc, + .dec_to_i16_try_unsafe, + .dec_to_i32_trunc, + .dec_to_i32_try_unsafe, + .dec_to_i64_trunc, + .dec_to_i64_try_unsafe, + .dec_to_i128_trunc, + .dec_to_i128_try_unsafe, + .dec_to_u8_trunc, + .dec_to_u8_try_unsafe, + .dec_to_u16_trunc, + .dec_to_u16_try_unsafe, + .dec_to_u32_trunc, + .dec_to_u32_try_unsafe, + .dec_to_u64_trunc, + .dec_to_u64_try_unsafe, + .dec_to_u128_trunc, + .dec_to_u128_try_unsafe, + .dec_to_f32_wrap, + .dec_to_f32_try_unsafe, + .dec_to_f64, + => &.{.borrow}, + }; + } + }; pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: DataSpan }; + pub const Span = extern struct { span: DataSpan }; /// A single branch of an if expression. /// Contains a condition expression and the body to execute if the condition is true. @@ -370,7 +1172,7 @@ pub const Expr = union(enum) { body: Expr.Idx, pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: base.DataSpan }; + pub const Span = extern struct { span: base.DataSpan }; }; /// A closure, which is a lambda expression that captures variables @@ -430,12 +1232,9 @@ pub const Expr = union(enum) { ge, // >= eq, // == ne, // != - pow, // ^ div_trunc, // // @"and", // and @"or", // or - pipe_forward, // |> - null_coalesce, // ? }; pub fn init(op: Op, lhs: Expr.Idx, rhs: Expr.Idx) Binop { @@ -466,15 +1265,14 @@ pub const Expr = union(enum) { pub fn pushToSExprTree(self: *const @This(), ir: *const ModuleEnv, tree: *SExprTree, expr_idx: Self.Idx) std.mem.Allocator.Error!void { switch (self.*) { - .e_int => |int_expr| { + .e_num => |int_expr| { const begin = tree.beginNode(); - try tree.pushStaticAtom("e-int"); + try tree.pushStaticAtom("e-num"); const region = ir.store.getExprRegion(expr_idx); try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); - const value_i128: i128 = @bitCast(int_expr.value.bytes); var value_buf: [40]u8 = undefined; - const value_str = std.fmt.bufPrint(&value_buf, "{}", .{value_i128}) catch "fmt_error"; + const value_str = int_expr.value.bufPrint(&value_buf) catch unreachable; try tree.pushStringPair("value", value_str); const attrs = tree.beginNode(); @@ -516,7 +1314,7 @@ pub const Expr = union(enum) { const attrs = tree.beginNode(); try tree.endNode(begin, attrs); }, - .e_frac_dec => |e| { + .e_dec => |e| { const begin = tree.beginNode(); try tree.pushStaticAtom("e-frac-dec"); const region = ir.store.getExprRegion(expr_idx); @@ -542,15 +1340,15 @@ pub const Expr = union(enum) { try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); var num_buf: [32]u8 = undefined; - const num_str = std.fmt.bufPrint(&num_buf, "{}", .{e.numerator}) catch "fmt_error"; + const num_str = std.fmt.bufPrint(&num_buf, "{}", .{e.value.numerator}) catch "fmt_error"; try tree.pushStringPair("numerator", num_str); var denom_buf: [32]u8 = undefined; - const denom_str = std.fmt.bufPrint(&denom_buf, "{}", .{e.denominator_power_of_ten}) catch "fmt_error"; + const denom_str = std.fmt.bufPrint(&denom_buf, "{}", .{e.value.denominator_power_of_ten}) catch "fmt_error"; try tree.pushStringPair("denominator-power-of-ten", denom_str); - const numerator_f64: f64 = @floatFromInt(e.numerator); - const denominator_f64: f64 = std.math.pow(f64, 10, @floatFromInt(e.denominator_power_of_ten)); + const numerator_f64: f64 = @floatFromInt(e.value.numerator); + const denominator_f64: f64 = std.math.pow(f64, 10, @floatFromInt(e.value.denominator_power_of_ten)); const value_f64 = numerator_f64 / denominator_f64; var value_buf: [512]u8 = undefined; @@ -649,15 +1447,36 @@ pub const Expr = union(enum) { try ir.appendRegionInfoToSExprTreeFromRegion(tree, e.region); const attrs = tree.beginNode(); - // Add module index - var buf: [32]u8 = undefined; - const module_idx_str = std.fmt.bufPrint(&buf, "{}", .{@intFromEnum(e.module_idx)}) catch unreachable; - try tree.pushStringPair("module-idx", module_idx_str); + const module_idx_int = @intFromEnum(e.module_idx); + std.debug.assert(module_idx_int < ir.imports.imports.items.items.len); + const string_lit_idx = ir.imports.imports.items.items[module_idx_int]; + const module_name = ir.common.strings.get(string_lit_idx); + // Special case: Builtin module is an implementation detail, print as (builtin) + if (std.mem.eql(u8, module_name, "Builtin")) { + const field_begin = tree.beginNode(); + try tree.pushStaticAtom("builtin"); + const field_attrs = tree.beginNode(); + try tree.endNode(field_begin, field_attrs); + } else { + try tree.pushStringPair("external-module", module_name); + } - // Add target node index - var buf2: [32]u8 = undefined; - const target_idx_str = std.fmt.bufPrint(&buf2, "{}", .{e.target_node_idx}) catch unreachable; - try tree.pushStringPair("target-node-idx", target_idx_str); + try tree.endNode(begin, attrs); + }, + .e_lookup_required => |e| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("e-lookup-required"); + const region = ir.store.getExprRegion(expr_idx); + try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); + const attrs = tree.beginNode(); + + const requires_items = ir.requires_types.items.items; + const idx = e.requires_idx.toU32(); + if (idx < requires_items.len) { + const required_type = requires_items[idx]; + const ident_name = ir.getIdent(required_type.ident); + try tree.pushStringPair("required-ident", ident_name); + } try tree.endNode(begin, attrs); }, @@ -714,12 +1533,10 @@ pub const Expr = union(enum) { const all_exprs = ir.store.exprSlice(c.args); - if (all_exprs.len > 0) { - try ir.store.getExpr(all_exprs[0]).pushToSExprTree(ir, tree, all_exprs[0]); - } + try ir.store.getExpr(c.func).pushToSExprTree(ir, tree, c.func); - if (all_exprs.len > 1) { - for (all_exprs[1..]) |arg_idx| { + if (all_exprs.len > 0) { + for (all_exprs[0..]) |arg_idx| { try ir.store.getExpr(arg_idx).pushToSExprTree(ir, tree, arg_idx); } } @@ -826,15 +1643,19 @@ pub const Expr = union(enum) { try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); const attrs = tree.beginNode(); - // Add module index - var buf: [32]u8 = undefined; - const module_idx_str = std.fmt.bufPrint(&buf, "{}", .{@intFromEnum(e.module_idx)}) catch unreachable; - try tree.pushStringPair("module-idx", module_idx_str); - - // Add target node index - var buf2: [32]u8 = undefined; - const target_idx_str = std.fmt.bufPrint(&buf2, "{}", .{e.target_node_idx}) catch unreachable; - try tree.pushStringPair("target-node-idx", target_idx_str); + const module_idx_int = @intFromEnum(e.module_idx); + std.debug.assert(module_idx_int < ir.imports.imports.items.items.len); + const string_lit_idx = ir.imports.imports.items.items[module_idx_int]; + const module_name = ir.common.strings.get(string_lit_idx); + // Special case: Builtin module is an implementation detail, print as (builtin) + if (std.mem.eql(u8, module_name, "Builtin")) { + const field_begin = tree.beginNode(); + try tree.pushStaticAtom("builtin"); + const field_attrs = tree.beginNode(); + try tree.endNode(field_begin, field_attrs); + } else { + try tree.pushStringPair("external-module", module_name); + } try ir.store.getExpr(e.backing_expr).pushToSExprTree(ir, tree, e.backing_expr); @@ -982,6 +1803,55 @@ pub const Expr = union(enum) { const attrs = tree.beginNode(); try tree.endNode(begin, attrs); }, + .e_anno_only => |_| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("e-anno-only"); + const region = ir.store.getExprRegion(expr_idx); + try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); + const attrs = tree.beginNode(); + try tree.endNode(begin, attrs); + }, + .e_hosted_lambda => |hosted| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("e-hosted-lambda"); + const symbol_name = ir.common.getIdent(hosted.symbol_name); + try tree.pushStringPair("symbol", symbol_name); + const region = ir.store.getExprRegion(expr_idx); + try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); + const attrs = tree.beginNode(); + + const args_begin = tree.beginNode(); + try tree.pushStaticAtom("args"); + const args_attrs = tree.beginNode(); + for (ir.store.slicePatterns(hosted.args)) |arg_idx| { + try ir.store.getPattern(arg_idx).pushToSExprTree(ir, tree, arg_idx); + } + try tree.endNode(args_begin, args_attrs); + + try tree.endNode(begin, attrs); + }, + .e_low_level_lambda => |low_level| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("e-low-level-lambda"); + const op_name = try std.fmt.allocPrint(ir.gpa, "{s}", .{@tagName(low_level.op)}); + defer ir.gpa.free(op_name); + try tree.pushStringPair("op", op_name); + const region = ir.store.getExprRegion(expr_idx); + try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); + const attrs = tree.beginNode(); + + const args_begin = tree.beginNode(); + try tree.pushStaticAtom("args"); + const args_attrs = tree.beginNode(); + for (ir.store.slicePatterns(low_level.args)) |arg_idx| { + try ir.store.getPattern(arg_idx).pushToSExprTree(ir, tree, arg_idx); + } + try tree.endNode(args_begin, args_attrs); + + try ir.store.getExpr(low_level.body).pushToSExprTree(ir, tree, low_level.body); + + try tree.endNode(begin, attrs); + }, .e_crash => |e| { const begin = tree.beginNode(); try tree.pushStaticAtom("e-crash"); @@ -1012,6 +1882,55 @@ pub const Expr = union(enum) { // Add body expression try ir.store.getExpr(expect_expr.body).pushToSExprTree(ir, tree, expect_expr.body); + try tree.endNode(begin, attrs); + }, + .e_return => |ret| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("e-return"); + const region = ir.store.getExprRegion(expr_idx); + try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); + const attrs = tree.beginNode(); + + // Add inner expression + try ir.store.getExpr(ret.expr).pushToSExprTree(ir, tree, ret.expr); + + try tree.endNode(begin, attrs); + }, + .e_type_var_dispatch => |tvd| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("e-type-var-dispatch"); + const region = ir.store.getExprRegion(expr_idx); + try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); + + // Add method name + const method_text = ir.getIdent(tvd.method_name); + try tree.pushStringPair("method", method_text); + + const attrs = tree.beginNode(); + + // Add arguments if any + for (ir.store.exprSlice(tvd.args)) |arg_idx| { + try ir.store.getExpr(arg_idx).pushToSExprTree(ir, tree, arg_idx); + } + + try tree.endNode(begin, attrs); + }, + .e_for => |for_expr| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("e-for"); + const region = ir.store.getExprRegion(expr_idx); + try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); + const attrs = tree.beginNode(); + + // Add pattern + try ir.store.getPattern(for_expr.patt).pushToSExprTree(ir, tree, for_expr.patt); + + // Add list expression + try ir.store.getExpr(for_expr.expr).pushToSExprTree(ir, tree, for_expr.expr); + + // Add body expression + try ir.store.getExpr(for_expr.body).pushToSExprTree(ir, tree, for_expr.body); + try tree.endNode(begin, attrs); }, } @@ -1043,7 +1962,7 @@ pub const Expr = union(enum) { exhaustive: TypeVar, pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: base.DataSpan }; + pub const Span = extern struct { span: base.DataSpan }; /// A single branch within a match expression. /// Contains patterns to match against, an optional guard condition, @@ -1102,7 +2021,7 @@ pub const Expr = union(enum) { } pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: DataSpan }; + pub const Span = extern struct { span: DataSpan }; }; /// A pattern within a match branch, which may be part of an OR pattern. @@ -1125,7 +2044,7 @@ pub const Expr = union(enum) { degenerate: bool, pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: base.DataSpan }; + pub const Span = extern struct { span: base.DataSpan }; }; pub fn pushToSExprTree(self: *const @This(), ir: *const ModuleEnv, tree: *SExprTree, region: Region) std.mem.Allocator.Error!void { @@ -1159,6 +2078,6 @@ pub const Expr = union(enum) { scope_depth: u32, pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: base.DataSpan }; + pub const Span = extern struct { span: base.DataSpan }; }; }; diff --git a/src/canonicalize/ExternalDecl.zig b/src/canonicalize/ExternalDecl.zig index f176f657a4..e0c8920df3 100644 --- a/src/canonicalize/ExternalDecl.zig +++ b/src/canonicalize/ExternalDecl.zig @@ -19,11 +19,9 @@ type_idx: ?u32, /// Index type for referencing external declarations in storage. pub const Idx = enum(u32) { _ }; /// A span of external declarations stored contiguously in memory. -pub const Span = struct { span: DataSpan }; +pub const Span = extern struct { span: DataSpan }; /// Converts this external declaration to an S-expression tree representation for debugging -pub fn pushToSExprTree(self: *const ExternalDecl, cir: anytype, tree: anytype) !void { - _ = self; - _ = cir; +pub fn pushToSExprTree(_: *const ExternalDecl, _: anytype, tree: anytype) !void { try tree.pushStaticAtom("external-decl-stub"); } diff --git a/src/canonicalize/HostedCompiler.zig b/src/canonicalize/HostedCompiler.zig new file mode 100644 index 0000000000..1b6b57a5fe --- /dev/null +++ b/src/canonicalize/HostedCompiler.zig @@ -0,0 +1,222 @@ +//! Compiler support for hosted functions in platform modules. +//! +//! This module handles the transformation of annotation-only declarations +//! into hosted lambda expressions that will be provided by the platform at runtime. + +const std = @import("std"); +const base = @import("base"); +const ModuleEnv = @import("ModuleEnv.zig"); +const CIR = @import("CIR.zig"); + +/// Replace all e_anno_only expressions in a Type Module with e_hosted_lambda operations (in-place). +/// This transforms standalone annotations into hosted lambda operations that will be +/// provided by the host application at runtime. +/// Returns a list of def indices that were modified. +pub fn replaceAnnoOnlyWithHosted(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) { + const gpa = env.gpa; + var modified_def_indices = std.ArrayList(CIR.Def.Idx).empty; + + // Ensure types array has entries for all existing nodes + // This is necessary because varFrom(node_idx) assumes type_var index == node index + const current_nodes = env.store.nodes.len(); + const current_types = env.types.len(); + if (current_types < current_nodes) { + // Fill the gap with fresh type variables + var i: u64 = current_types; + while (i < current_nodes) : (i += 1) { + _ = env.types.fresh() catch unreachable; + } + } + + // Iterate through all defs and replace ALL anno-only defs with hosted implementations + const all_defs = env.store.sliceDefs(env.all_defs); + for (all_defs) |def_idx| { + const def = env.store.getDef(def_idx); + const expr = env.store.getExpr(def.expr); + + // Check if this is an anno-only def (e_anno_only expression) + if (expr == .e_anno_only and def.annotation != null) { + // Get the identifier from the pattern + const pattern = env.store.getPattern(def.pattern); + if (pattern == .assign) { + const full_ident = pattern.assign.ident; + + // Get the region from the original def for better error messages + const def_node_idx: @TypeOf(env.store.nodes).Idx = @enumFromInt(@intFromEnum(def_idx)); + const def_region = env.store.getRegionAt(def_node_idx); + + // Extract the unqualified name (e.g., "line!" from "Stdout.line!") + // The pattern might contain a qualified name, but we need the unqualified one + const full_name = env.getIdent(full_ident); + const unqualified_name = if (std.mem.lastIndexOfScalar(u8, full_name, '.')) |dot_idx| + full_name[dot_idx + 1 ..] + else + full_name; + const ident = env.common.findIdent(unqualified_name) orelse try env.common.insertIdent(gpa, base.Ident.for_text(unqualified_name)); + + // Extract the number of arguments from the annotation + const annotation = env.store.getAnnotation(def.annotation.?); + const type_anno = env.store.getTypeAnno(annotation.anno); + + const num_args: usize = if (type_anno == .@"fn") blk: { + const func_type = type_anno.@"fn"; + const args_slice = env.store.sliceTypeAnnos(func_type.args); + + // Check if single argument is empty tuple () - if so, create 0 params + if (args_slice.len == 1) { + const first_arg = env.store.getTypeAnno(args_slice[0]); + if (first_arg == .tuple) { + if (first_arg.tuple.elems.span.len == 0) { + break :blk 0; // () means 0 parameters + } + } + } + + break :blk args_slice.len; + } else 0; + + // Create dummy parameter patterns for the lambda (one for each argument) + // Use the def's region for better error diagnostics + const patterns_start = env.store.scratchTop("patterns"); + var arg_i: usize = 0; + while (arg_i < num_args) : (arg_i += 1) { + const arg_name = try std.fmt.allocPrint(gpa, "_arg{}", .{arg_i}); + defer gpa.free(arg_name); + const arg_ident = env.common.findIdent(arg_name) orelse try env.common.insertIdent(gpa, base.Ident.for_text(arg_name)); + const arg_pattern_idx = try env.addPattern(.{ .assign = .{ .ident = arg_ident } }, def_region); + try env.store.scratch.?.patterns.append(arg_pattern_idx); + } + const args_span = CIR.Pattern.Span{ .span = .{ .start = @intCast(patterns_start), .len = @intCast(num_args) } }; + + // Create an e_crash body that crashes when the function is called in the interpreter. + // This is a placeholder - hosted functions are provided by the platform's native code, + // so this body should never be evaluated during normal compilation/execution. + const crash_msg = try env.insertString("Hosted functions cannot be called in the interpreter"); + const body_idx = try env.addExpr(.{ .e_crash = .{ .msg = crash_msg } }, def_region); + + // Ensure types array has entries for all new expressions + const body_int = @intFromEnum(body_idx); + while (env.types.len() <= body_int) { + _ = try env.types.fresh(); + } + + // Create e_hosted_lambda expression + const expr_idx = try env.addExpr(.{ + .e_hosted_lambda = .{ + .symbol_name = ident, + .index = 0, // Placeholder; will be assigned during sorting pass + .args = args_span, + .body = body_idx, + }, + }, def_region); + + // Ensure types array has an entry for this new expression + const expr_int = @intFromEnum(expr_idx); + while (env.types.len() <= expr_int) { + _ = try env.types.fresh(); + } + + // Now replace the e_anno_only expression with the e_hosted_lambda + // We need to modify the def's expr field in extra_data (NOT data_2!) + // The expr is stored in extra_data[extra_start + 1] + // (reuse def_node_idx from above) + const def_node = env.store.nodes.get(def_node_idx); + const extra_start = def_node.data_1; + + env.store.extra_data.items.items[extra_start + 1] = @intFromEnum(expr_idx); + + // Track this modified def index + try modified_def_indices.append(gpa, def_idx); + } + } + } + + return modified_def_indices; +} + +/// Information about a hosted function for sorting and indexing +pub const HostedFunctionInfo = struct { + symbol_name: base.Ident.Idx, + expr_idx: CIR.Expr.Idx, + name_text: []const u8, // For sorting +}; + +/// Collect all hosted functions from the module (transitively through imports) +/// and sort them alphabetically by fully-qualified name (with `!` stripped). +pub fn collectAndSortHostedFunctions(env: *ModuleEnv) !std.ArrayList(HostedFunctionInfo) { + var hosted_fns = std.ArrayList(HostedFunctionInfo).empty; + + // Use a hash set to deduplicate by symbol identifier (not string comparison) + var seen_symbols = std.AutoHashMap(base.Ident.Idx, void).init(env.gpa); + defer seen_symbols.deinit(); + + // Iterate through all defs to find e_hosted_lambda expressions + const all_defs = env.store.sliceDefs(env.all_defs); + for (all_defs) |def_idx| { + const def = env.store.getDef(def_idx); + const expr = env.store.getExpr(def.expr); + + if (expr == .e_hosted_lambda) { + const hosted = expr.e_hosted_lambda; + const local_name = env.getIdent(hosted.symbol_name); + + // Deduplicate based on symbol identifier + const gop = try seen_symbols.getOrPut(hosted.symbol_name); + if (gop.found_existing) { + continue; // Skip duplicate + } + + // Build fully-qualified name: "ModuleName.functionName" + // Strip the .roc extension from module name (e.g., "Stdout.roc" -> "Stdout") + var module_name = env.module_name; + + if (std.mem.endsWith(u8, module_name, ".roc")) { + module_name = module_name[0 .. module_name.len - 4]; + } + const qualified_name = try std.fmt.allocPrint(env.gpa, "{s}.{s}", .{ module_name, local_name }); + defer env.gpa.free(qualified_name); + + // Strip the `!` suffix for sorting (e.g., "Stdout.line!" -> "Stdout.line") + const stripped_name = if (std.mem.endsWith(u8, qualified_name, "!")) + qualified_name[0 .. qualified_name.len - 1] + else + qualified_name; + + // Allocate a copy for storage + const name_copy = try env.gpa.dupe(u8, stripped_name); + + try hosted_fns.append(env.gpa, .{ + .symbol_name = hosted.symbol_name, + .expr_idx = def.expr, + .name_text = name_copy, + }); + } + } + + // Sort alphabetically by stripped qualified name + const SortContext = struct { + pub fn lessThan(_: void, a: HostedFunctionInfo, b: HostedFunctionInfo) bool { + return std.mem.order(u8, a.name_text, b.name_text) == .lt; + } + }; + std.mem.sort(HostedFunctionInfo, hosted_fns.items, {}, SortContext.lessThan); + + return hosted_fns; +} + +/// Assign indices to e_hosted_lambda expressions based on sorted order +pub fn assignHostedIndices(env: *ModuleEnv, sorted_fns: []const HostedFunctionInfo) !void { + for (sorted_fns, 0..) |fn_info, index| { + // Get the expression node (Expr.Idx and Node.Idx have same underlying representation) + const expr_node_idx = @as(@TypeOf(env.store.nodes).Idx, @enumFromInt(@intFromEnum(fn_info.expr_idx))); + var expr_node = env.store.nodes.get(expr_node_idx); + + // For e_hosted_lambda nodes: + // data_1 = symbol_name (Ident.Idx via @bitCast) + // data_2 = index (u32) <- We set this here + // data_3 = extra_data pointer (for args and body) + expr_node.data_2 = @intCast(index); + + env.store.nodes.set(expr_node_idx, expr_node); + } +} diff --git a/src/canonicalize/ModuleEnv.zig b/src/canonicalize/ModuleEnv.zig index a91ca3230d..e52c74b190 100644 --- a/src/canonicalize/ModuleEnv.zig +++ b/src/canonicalize/ModuleEnv.zig @@ -14,9 +14,11 @@ const base = @import("base"); const Node = @import("Node.zig"); const NodeStore = @import("NodeStore.zig"); const CIR = @import("CIR.zig"); +const DependencyGraph = @import("DependencyGraph.zig"); const TypeWriter = types_mod.TypeWriter; const CompactWriter = collections.CompactWriter; +const SortedArrayBuilder = collections.SortedArrayBuilder; const CommonEnv = base.CommonEnv; const Ident = base.Ident; const StringLiteral = base.StringLiteral; @@ -29,18 +31,391 @@ const TypeStore = types_mod.Store; const Self = @This(); +/// The kind of module being canonicalized, set during header processing +pub const ModuleKind = union(enum) { + type_module: Ident.Idx, // Holds the main type identifier for type modules + default_app, + app, + package, + platform, + hosted, + deprecated_module, + malformed, + + /// Extern-compatible tag for serialization + pub const Tag = enum(u32) { + type_module, + default_app, + app, + package, + platform, + hosted, + deprecated_module, + malformed, + }; + + /// Extern-compatible payload union for serialization + pub const Payload = extern union { + type_module_ident: Ident.Idx, + none: u32, + }; + + /// Extern-compatible serialized form + pub const Serialized = extern struct { + tag: Tag, + payload: Payload, + + pub fn encode(kind: ModuleKind) @This() { + return switch (kind) { + .type_module => |idx| .{ .tag = .type_module, .payload = .{ .type_module_ident = idx } }, + .default_app => .{ .tag = .default_app, .payload = .{ .none = 0 } }, + .app => .{ .tag = .app, .payload = .{ .none = 0 } }, + .package => .{ .tag = .package, .payload = .{ .none = 0 } }, + .platform => .{ .tag = .platform, .payload = .{ .none = 0 } }, + .hosted => .{ .tag = .hosted, .payload = .{ .none = 0 } }, + .deprecated_module => .{ .tag = .deprecated_module, .payload = .{ .none = 0 } }, + .malformed => .{ .tag = .malformed, .payload = .{ .none = 0 } }, + }; + } + + pub fn decode(self: @This()) ModuleKind { + return switch (self.tag) { + .type_module => .{ .type_module = self.payload.type_module_ident }, + .default_app => .default_app, + .app => .app, + .package => .package, + .platform => .platform, + .hosted => .hosted, + .deprecated_module => .deprecated_module, + .malformed => .malformed, + }; + } + }; +}; + +/// Well-known identifiers that are interned once and reused throughout compilation. +/// These are needed for type checking, operator desugaring, and layout generation. +/// This is an extern struct so it can be embedded in serialized ModuleEnv. +pub const CommonIdents = extern struct { + // Method names for operator desugaring + from_int_digits: Ident.Idx, + from_dec_digits: Ident.Idx, + plus: Ident.Idx, + minus: Ident.Idx, + times: Ident.Idx, + div_by: Ident.Idx, + div_trunc_by: Ident.Idx, + rem_by: Ident.Idx, + negate: Ident.Idx, + abs: Ident.Idx, + abs_diff: Ident.Idx, + not: Ident.Idx, + is_lt: Ident.Idx, + is_lte: Ident.Idx, + is_gt: Ident.Idx, + is_gte: Ident.Idx, + is_eq: Ident.Idx, + + // Type/module names + @"try": Ident.Idx, + out_of_range: Ident.Idx, + builtin_module: Ident.Idx, + str: Ident.Idx, + list: Ident.Idx, + box: Ident.Idx, + + // Unqualified builtin type names (for checking if a type name shadows a builtin) + num: Ident.Idx, + u8: Ident.Idx, + u16: Ident.Idx, + u32: Ident.Idx, + u64: Ident.Idx, + u128: Ident.Idx, + i8: Ident.Idx, + i16: Ident.Idx, + i32: Ident.Idx, + i64: Ident.Idx, + i128: Ident.Idx, + f32: Ident.Idx, + f64: Ident.Idx, + dec: Ident.Idx, + + // Fully-qualified type identifiers for type checking and layout generation + builtin_try: Ident.Idx, + builtin_numeral: Ident.Idx, + builtin_str: Ident.Idx, + u8_type: Ident.Idx, + i8_type: Ident.Idx, + u16_type: Ident.Idx, + i16_type: Ident.Idx, + u32_type: Ident.Idx, + i32_type: Ident.Idx, + u64_type: Ident.Idx, + i64_type: Ident.Idx, + u128_type: Ident.Idx, + i128_type: Ident.Idx, + f32_type: Ident.Idx, + f64_type: Ident.Idx, + dec_type: Ident.Idx, + + // Field/tag names used during type checking and evaluation + before_dot: Ident.Idx, + after_dot: Ident.Idx, + provided_by_compiler: Ident.Idx, + tag: Ident.Idx, + payload: Ident.Idx, + is_negative: Ident.Idx, + digits_before_pt: Ident.Idx, + digits_after_pt: Ident.Idx, + box_method: Ident.Idx, + unbox_method: Ident.Idx, + // Fully qualified Box intrinsic method names + builtin_box_box: Ident.Idx, + builtin_box_unbox: Ident.Idx, + to_inspect: Ident.Idx, + ok: Ident.Idx, + err: Ident.Idx, + from_numeral: Ident.Idx, + true_tag: Ident.Idx, + false_tag: Ident.Idx, + // from_utf8 result fields + byte_index: Ident.Idx, + string: Ident.Idx, + is_ok: Ident.Idx, + problem_code: Ident.Idx, + // from_utf8 error payload fields (BadUtf8 record) + problem: Ident.Idx, + index: Ident.Idx, + // Synthetic identifiers for ? operator desugaring + question_ok: Ident.Idx, + question_err: Ident.Idx, + + /// Insert all well-known identifiers into a CommonEnv. + /// Use this when creating a fresh ModuleEnv from scratch. + pub fn insert(gpa: std.mem.Allocator, common: *CommonEnv) std.mem.Allocator.Error!CommonIdents { + return .{ + .from_int_digits = try common.insertIdent(gpa, Ident.for_text(Ident.FROM_INT_DIGITS_METHOD_NAME)), + .from_dec_digits = try common.insertIdent(gpa, Ident.for_text(Ident.FROM_DEC_DIGITS_METHOD_NAME)), + .plus = try common.insertIdent(gpa, Ident.for_text(Ident.PLUS_METHOD_NAME)), + .minus = try common.insertIdent(gpa, Ident.for_text("minus")), + .times = try common.insertIdent(gpa, Ident.for_text("times")), + .div_by = try common.insertIdent(gpa, Ident.for_text("div_by")), + .div_trunc_by = try common.insertIdent(gpa, Ident.for_text("div_trunc_by")), + .rem_by = try common.insertIdent(gpa, Ident.for_text("rem_by")), + .negate = try common.insertIdent(gpa, Ident.for_text(Ident.NEGATE_METHOD_NAME)), + .abs = try common.insertIdent(gpa, Ident.for_text("abs")), + .abs_diff = try common.insertIdent(gpa, Ident.for_text("abs_diff")), + .not = try common.insertIdent(gpa, Ident.for_text("not")), + .is_lt = try common.insertIdent(gpa, Ident.for_text("is_lt")), + .is_lte = try common.insertIdent(gpa, Ident.for_text("is_lte")), + .is_gt = try common.insertIdent(gpa, Ident.for_text("is_gt")), + .is_gte = try common.insertIdent(gpa, Ident.for_text("is_gte")), + .is_eq = try common.insertIdent(gpa, Ident.for_text("is_eq")), + .@"try" = try common.insertIdent(gpa, Ident.for_text("Try")), + .out_of_range = try common.insertIdent(gpa, Ident.for_text("OutOfRange")), + .builtin_module = try common.insertIdent(gpa, Ident.for_text("Builtin")), + .str = try common.insertIdent(gpa, Ident.for_text("Str")), + .list = try common.insertIdent(gpa, Ident.for_text("List")), + .box = try common.insertIdent(gpa, Ident.for_text("Box")), + // Unqualified builtin type names + .num = try common.insertIdent(gpa, Ident.for_text("Num")), + .u8 = try common.insertIdent(gpa, Ident.for_text("U8")), + .u16 = try common.insertIdent(gpa, Ident.for_text("U16")), + .u32 = try common.insertIdent(gpa, Ident.for_text("U32")), + .u64 = try common.insertIdent(gpa, Ident.for_text("U64")), + .u128 = try common.insertIdent(gpa, Ident.for_text("U128")), + .i8 = try common.insertIdent(gpa, Ident.for_text("I8")), + .i16 = try common.insertIdent(gpa, Ident.for_text("I16")), + .i32 = try common.insertIdent(gpa, Ident.for_text("I32")), + .i64 = try common.insertIdent(gpa, Ident.for_text("I64")), + .i128 = try common.insertIdent(gpa, Ident.for_text("I128")), + .f32 = try common.insertIdent(gpa, Ident.for_text("F32")), + .f64 = try common.insertIdent(gpa, Ident.for_text("F64")), + .dec = try common.insertIdent(gpa, Ident.for_text("Dec")), + .builtin_try = try common.insertIdent(gpa, Ident.for_text("Try")), + .builtin_numeral = try common.insertIdent(gpa, Ident.for_text("Num.Numeral")), + .builtin_str = try common.insertIdent(gpa, Ident.for_text("Builtin.Str")), + .u8_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U8")), + .i8_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I8")), + .u16_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U16")), + .i16_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I16")), + .u32_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U32")), + .i32_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I32")), + .u64_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U64")), + .i64_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I64")), + .u128_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.U128")), + .i128_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.I128")), + .f32_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.F32")), + .f64_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.F64")), + .dec_type = try common.insertIdent(gpa, Ident.for_text("Builtin.Num.Dec")), + .before_dot = try common.insertIdent(gpa, Ident.for_text("before_dot")), + .after_dot = try common.insertIdent(gpa, Ident.for_text("after_dot")), + .provided_by_compiler = try common.insertIdent(gpa, Ident.for_text("ProvidedByCompiler")), + .tag = try common.insertIdent(gpa, Ident.for_text("tag")), + .payload = try common.insertIdent(gpa, Ident.for_text("payload")), + .is_negative = try common.insertIdent(gpa, Ident.for_text("is_negative")), + .digits_before_pt = try common.insertIdent(gpa, Ident.for_text("digits_before_pt")), + .digits_after_pt = try common.insertIdent(gpa, Ident.for_text("digits_after_pt")), + .box_method = try common.insertIdent(gpa, Ident.for_text("box")), + .unbox_method = try common.insertIdent(gpa, Ident.for_text("unbox")), + // Fully qualified Box intrinsic method names + .builtin_box_box = try common.insertIdent(gpa, Ident.for_text("Builtin.Box.box")), + .builtin_box_unbox = try common.insertIdent(gpa, Ident.for_text("Builtin.Box.unbox")), + .to_inspect = try common.insertIdent(gpa, Ident.for_text("to_inspect")), + .ok = try common.insertIdent(gpa, Ident.for_text("Ok")), + .err = try common.insertIdent(gpa, Ident.for_text("Err")), + .from_numeral = try common.insertIdent(gpa, Ident.for_text("from_numeral")), + .true_tag = try common.insertIdent(gpa, Ident.for_text("True")), + .false_tag = try common.insertIdent(gpa, Ident.for_text("False")), + // from_utf8 result fields + .byte_index = try common.insertIdent(gpa, Ident.for_text("byte_index")), + .string = try common.insertIdent(gpa, Ident.for_text("string")), + .is_ok = try common.insertIdent(gpa, Ident.for_text("is_ok")), + .problem_code = try common.insertIdent(gpa, Ident.for_text("problem_code")), + // from_utf8 error payload fields (BadUtf8 record) + .problem = try common.insertIdent(gpa, Ident.for_text("problem")), + .index = try common.insertIdent(gpa, Ident.for_text("index")), + // Synthetic identifiers for ? operator desugaring + .question_ok = try common.insertIdent(gpa, Ident.for_text("#ok")), + .question_err = try common.insertIdent(gpa, Ident.for_text("#err")), + }; + } + + /// Find all well-known identifiers in a CommonEnv that has already interned them. + /// Use this when loading a pre-compiled module where identifiers are already present. + /// Panics if any identifier is not found (indicates corrupted/incompatible pre-compiled data). + pub fn find(common: *const CommonEnv) CommonIdents { + return .{ + .from_int_digits = common.findIdent(Ident.FROM_INT_DIGITS_METHOD_NAME) orelse unreachable, + .from_dec_digits = common.findIdent(Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse unreachable, + .plus = common.findIdent(Ident.PLUS_METHOD_NAME) orelse unreachable, + .minus = common.findIdent("minus") orelse unreachable, + .times = common.findIdent("times") orelse unreachable, + .div_by = common.findIdent("div_by") orelse unreachable, + .div_trunc_by = common.findIdent("div_trunc_by") orelse unreachable, + .rem_by = common.findIdent("rem_by") orelse unreachable, + .negate = common.findIdent(Ident.NEGATE_METHOD_NAME) orelse unreachable, + .abs = common.findIdent("abs") orelse unreachable, + .abs_diff = common.findIdent("abs_diff") orelse unreachable, + .not = common.findIdent("not") orelse unreachable, + .is_lt = common.findIdent("is_lt") orelse unreachable, + .is_lte = common.findIdent("is_lte") orelse unreachable, + .is_gt = common.findIdent("is_gt") orelse unreachable, + .is_gte = common.findIdent("is_gte") orelse unreachable, + .is_eq = common.findIdent("is_eq") orelse unreachable, + .@"try" = common.findIdent("Try") orelse unreachable, + .out_of_range = common.findIdent("OutOfRange") orelse unreachable, + .builtin_module = common.findIdent("Builtin") orelse unreachable, + .str = common.findIdent("Str") orelse unreachable, + .list = common.findIdent("List") orelse unreachable, + .box = common.findIdent("Box") orelse unreachable, + // Unqualified builtin type names + .num = common.findIdent("Num") orelse unreachable, + .u8 = common.findIdent("U8") orelse unreachable, + .u16 = common.findIdent("U16") orelse unreachable, + .u32 = common.findIdent("U32") orelse unreachable, + .u64 = common.findIdent("U64") orelse unreachable, + .u128 = common.findIdent("U128") orelse unreachable, + .i8 = common.findIdent("I8") orelse unreachable, + .i16 = common.findIdent("I16") orelse unreachable, + .i32 = common.findIdent("I32") orelse unreachable, + .i64 = common.findIdent("I64") orelse unreachable, + .i128 = common.findIdent("I128") orelse unreachable, + .f32 = common.findIdent("F32") orelse unreachable, + .f64 = common.findIdent("F64") orelse unreachable, + .dec = common.findIdent("Dec") orelse unreachable, + .builtin_try = common.findIdent("Try") orelse unreachable, + .builtin_numeral = common.findIdent("Num.Numeral") orelse unreachable, + .builtin_str = common.findIdent("Builtin.Str") orelse unreachable, + .u8_type = common.findIdent("Builtin.Num.U8") orelse unreachable, + .i8_type = common.findIdent("Builtin.Num.I8") orelse unreachable, + .u16_type = common.findIdent("Builtin.Num.U16") orelse unreachable, + .i16_type = common.findIdent("Builtin.Num.I16") orelse unreachable, + .u32_type = common.findIdent("Builtin.Num.U32") orelse unreachable, + .i32_type = common.findIdent("Builtin.Num.I32") orelse unreachable, + .u64_type = common.findIdent("Builtin.Num.U64") orelse unreachable, + .i64_type = common.findIdent("Builtin.Num.I64") orelse unreachable, + .u128_type = common.findIdent("Builtin.Num.U128") orelse unreachable, + .i128_type = common.findIdent("Builtin.Num.I128") orelse unreachable, + .f32_type = common.findIdent("Builtin.Num.F32") orelse unreachable, + .f64_type = common.findIdent("Builtin.Num.F64") orelse unreachable, + .dec_type = common.findIdent("Builtin.Num.Dec") orelse unreachable, + .before_dot = common.findIdent("before_dot") orelse unreachable, + .after_dot = common.findIdent("after_dot") orelse unreachable, + .provided_by_compiler = common.findIdent("ProvidedByCompiler") orelse unreachable, + .tag = common.findIdent("tag") orelse unreachable, + .payload = common.findIdent("payload") orelse unreachable, + .is_negative = common.findIdent("is_negative") orelse unreachable, + .digits_before_pt = common.findIdent("digits_before_pt") orelse unreachable, + .digits_after_pt = common.findIdent("digits_after_pt") orelse unreachable, + .box_method = common.findIdent("box") orelse unreachable, + .unbox_method = common.findIdent("unbox") orelse unreachable, + // Fully qualified Box intrinsic method names + .builtin_box_box = common.findIdent("Builtin.Box.box") orelse unreachable, + .builtin_box_unbox = common.findIdent("Builtin.Box.unbox") orelse unreachable, + .to_inspect = common.findIdent("to_inspect") orelse unreachable, + .ok = common.findIdent("Ok") orelse unreachable, + .err = common.findIdent("Err") orelse unreachable, + .from_numeral = common.findIdent("from_numeral") orelse unreachable, + .true_tag = common.findIdent("True") orelse unreachable, + .false_tag = common.findIdent("False") orelse unreachable, + // from_utf8 result fields + .byte_index = common.findIdent("byte_index") orelse unreachable, + .string = common.findIdent("string") orelse unreachable, + .is_ok = common.findIdent("is_ok") orelse unreachable, + .problem_code = common.findIdent("problem_code") orelse unreachable, + // from_utf8 error payload fields (BadUtf8 record) + .problem = common.findIdent("problem") orelse unreachable, + .index = common.findIdent("index") orelse unreachable, + // Synthetic identifiers for ? operator desugaring + .question_ok = common.findIdent("#ok") orelse unreachable, + .question_err = common.findIdent("#err") orelse unreachable, + }; + } +}; + +/// Key for method identifier lookup: (type_ident, method_ident) pair. +pub const MethodKey = packed struct(u64) { + type_ident: Ident.Idx, + method_ident: Ident.Idx, +}; + +/// Mapping from (type_ident, method_ident) pairs to their qualified method ident. +/// This enables O(log n) index-based method lookup instead of O(n) string comparison. +/// The value is the qualified method ident (e.g., "Bool.is_eq" for type "Bool" and method "is_eq"). +/// +/// This is populated during canonicalization when methods are defined in associated blocks. +pub const MethodIdents = SortedArrayBuilder(MethodKey, Ident.Idx); + gpa: std.mem.Allocator, common: CommonEnv, types: TypeStore, -// ===== Module compilation fields ===== +// Module compilation fields // NOTE: These fields are populated during canonicalization and preserved for later use +/// The kind of module (type_module, app, etc.) - set during canonicalization +module_kind: ModuleKind, /// All the definitions in the module (populated by canonicalization) all_defs: CIR.Def.Span, /// All the top-level statements in the module (populated by canonicalization) all_statements: CIR.Statement.Span, +/// Definitions that are exported by this module (populated by canonicalization) +exports: CIR.Def.Span, +/// Required type signatures for platform modules (from `requires { main! : () => {} }`) +/// Maps identifier names to their expected type annotations. +/// Empty for non-platform modules. +requires_types: RequiredType.SafeList, +/// Type alias mappings from for-clauses in requires declarations. +/// Stores (alias_name, rigid_name) pairs like (Model, model). +for_clause_aliases: ForClauseAlias.SafeList, +/// Rigid type variable mappings from platform for-clause after unification. +/// Maps rigid names (e.g., "model") to their resolved type variables in the app's type store. +/// Populated during checkPlatformRequirements when the platform has a for-clause. +rigid_vars: std.AutoHashMapUnmanaged(Ident.Idx, TypeVar), +/// All builtin stmts (temporary until module imports are working) +builtin_statements: CIR.Statement.Span, /// All external declarations referenced in this module external_decls: CIR.ExternalDecl.SafeList, /// Store for interned module imports @@ -48,44 +423,155 @@ imports: CIR.Import.Store, /// The module's name as a string /// This is needed for import resolution to match import names to modules module_name: []const u8, +/// The module's name as an interned identifier (for fast comparisons) +module_name_idx: Ident.Idx, /// Diagnostics collected during canonicalization (optional) diagnostics: CIR.Diagnostic.Span, /// Stores the raw nodes which represent the intermediate representation /// Uses an efficient data structure, and provides helpers for storing and retrieving nodes. store: NodeStore, +/// Dependency analysis results (evaluation order for defs) +/// Set after canonicalization completes. Must not be accessed before then. +evaluation_order: ?*DependencyGraph.EvaluationOrder, + +/// Well-known identifiers for type checking, operator desugaring, and layout generation. +/// Interned once during init to avoid repeated string comparisons. +idents: CommonIdents, + +/// Deferred numeric literals collected during type checking +/// These will be validated during comptime evaluation +deferred_numeric_literals: DeferredNumericLiteral.SafeList, + +/// Import mapping for type display names in error messages. +/// Maps fully-qualified type identifiers to their shortest display names based on imports. +/// Built during canonicalization when processing import statements. +/// Example: "MyModule.Foo" -> "F" if user has `import MyModule exposing [Foo as F]` +import_mapping: types_mod.import_mapping.ImportMapping, + +/// Mapping from (type_ident, method_ident) pairs to qualified method idents. +/// Enables O(1) index-based method lookup during type checking and evaluation. +/// Populated during canonicalization when methods are defined in associated blocks. +method_idents: MethodIdents, + +/// Deferred numeric literal for compile-time validation +pub const DeferredNumericLiteral = struct { + expr_idx: CIR.Expr.Idx, + type_var: TypeVar, + constraint: types_mod.StaticDispatchConstraint, + region: Region, + + pub const SafeList = collections.SafeList(@This()); +}; + +/// A type alias mapping from a for-clause: [Model : model] +/// Maps an alias name (Model) to a rigid variable name (model) +pub const ForClauseAlias = struct { + /// The alias name (e.g., "Model") - to be looked up in the app + alias_name: Ident.Idx, + /// The rigid variable name (e.g., "model") - the rigid in the required type + rigid_name: Ident.Idx, + /// The type annotation of this alias stmt + alias_stmt_idx: CIR.Statement.Idx, + + pub const SafeList = collections.SafeList(@This()); +}; + +/// Required type for platform modules - maps an identifier to its expected type annotation. +/// Used to enforce that apps provide values matching the platform's required types. +pub const RequiredType = struct { + /// The identifier name (e.g., "main!") + ident: Ident.Idx, + /// The canonicalized type annotation for this required value + type_anno: CIR.TypeAnno.Idx, + /// Region of the requirement for error reporting + region: Region, + /// Type alias mappings from the for-clause (e.g., [Model : model]) + /// These specify which app type aliases should be substituted for which rigids + type_aliases: ForClauseAlias.SafeList.Range, + + pub const SafeList = collections.SafeList(@This()); +}; + +/// Relocate all pointers in the ModuleEnv by the given offset. +/// This is used when loading a ModuleEnv from shared memory at a different address. +pub fn relocate(self: *Self, offset: isize) void { + // Relocate all sub-structures that contain pointers + self.common.relocate(offset); + self.types.relocate(offset); + self.external_decls.relocate(offset); + self.requires_types.relocate(offset); + self.for_clause_aliases.relocate(offset); + self.imports.relocate(offset); + self.store.relocate(offset); + self.deferred_numeric_literals.relocate(offset); + self.method_idents.relocate(offset); + + // Relocate the module_name pointer if it's not empty + if (self.module_name.len > 0) { + const old_ptr = @intFromPtr(self.module_name.ptr); + const new_ptr = @as(isize, @intCast(old_ptr)) + offset; + self.module_name.ptr = @ptrFromInt(@as(usize, @intCast(new_ptr))); + } +} + /// Initialize the compilation fields in an existing ModuleEnv -pub fn initCIRFields(self: *Self, gpa: std.mem.Allocator, module_name: []const u8) !void { - _ = gpa; // unused since we don't create new allocations +pub fn initCIRFields(self: *Self, module_name: []const u8) !void { + self.module_kind = .deprecated_module; // Placeholder - set to actual kind during header canonicalization self.all_defs = .{ .span = .{ .start = 0, .len = 0 } }; self.all_statements = .{ .span = .{ .start = 0, .len = 0 } }; + self.exports = .{ .span = .{ .start = 0, .len = 0 } }; + self.builtin_statements = .{ .span = .{ .start = 0, .len = 0 } }; // Note: external_decls already exists from ModuleEnv.init(), so we don't create a new one self.imports = CIR.Import.Store.init(); self.module_name = module_name; + self.module_name_idx = try self.insertIdent(Ident.for_text(module_name)); self.diagnostics = CIR.Diagnostic.Span{ .span = base.DataSpan{ .start = 0, .len = 0 } }; // Note: self.store already exists from ModuleEnv.init(), so we don't create a new one + self.evaluation_order = null; // Will be set after canonicalization completes } /// Alias for initCIRFields for backwards compatibility with tests -pub fn initModuleEnvFields(self: *Self, gpa: std.mem.Allocator, module_name: []const u8) !void { - return self.initCIRFields(gpa, module_name); +pub fn initModuleEnvFields(self: *Self, module_name: []const u8) !void { + return self.initCIRFields(module_name); } -/// Initialize the module environment. +/// Initialize the module environment with capacity heuristics based on source size. pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error!Self { - // TODO: maybe wire in smarter default based on the initial input text size. + var common = try CommonEnv.init(gpa, source); + const idents = try CommonIdents.insert(gpa, &common); + + // Use source-based heuristics for initial capacities + // Typical Roc code generates ~1 node per 20 bytes, ~1 type per 50 bytes + // Use generous minimums to avoid too many reallocations for small files + const source_len = source.len; + const node_capacity = @max(1024, @min(100_000, source_len / 20)); + const type_capacity = @max(2048, @min(50_000, source_len / 50)); + const var_capacity = @max(512, @min(10_000, source_len / 100)); return Self{ .gpa = gpa, - .common = try CommonEnv.init(gpa, source), - .types = try TypeStore.initCapacity(gpa, 2048, 512), + .common = common, + .types = try TypeStore.initCapacity(gpa, type_capacity, var_capacity), + .module_kind = .deprecated_module, // Placeholder - set to actual kind during header canonicalization .all_defs = .{ .span = .{ .start = 0, .len = 0 } }, .all_statements = .{ .span = .{ .start = 0, .len = 0 } }, + .exports = .{ .span = .{ .start = 0, .len = 0 } }, + .requires_types = try RequiredType.SafeList.initCapacity(gpa, 4), + .for_clause_aliases = try ForClauseAlias.SafeList.initCapacity(gpa, 4), + .rigid_vars = std.AutoHashMapUnmanaged(Ident.Idx, TypeVar){}, + .builtin_statements = .{ .span = .{ .start = 0, .len = 0 } }, .external_decls = try CIR.ExternalDecl.SafeList.initCapacity(gpa, 16), .imports = CIR.Import.Store.init(), - .module_name = "", // Will be set later during canonicalization + .module_name = undefined, // Will be set later during canonicalization + .module_name_idx = Ident.Idx.NONE, // Will be set later during canonicalization .diagnostics = CIR.Diagnostic.Span{ .span = base.DataSpan{ .start = 0, .len = 0 } }, - .store = try NodeStore.initCapacity(gpa, 10_000), // Default node store capacity + .store = try NodeStore.initCapacity(gpa, node_capacity), + .evaluation_order = null, // Will be set after canonicalization completes + .idents = idents, + .deferred_numeric_literals = try DeferredNumericLiteral.SafeList.initCapacity(gpa, 32), + .import_mapping = types_mod.import_mapping.ImportMapping.init(gpa), + .method_idents = MethodIdents.init(), }; } @@ -94,33 +580,35 @@ pub fn deinit(self: *Self) void { self.common.deinit(self.gpa); self.types.deinit(); self.external_decls.deinit(self.gpa); + self.requires_types.deinit(self.gpa); + self.for_clause_aliases.deinit(self.gpa); + self.rigid_vars.deinit(self.gpa); self.imports.deinit(self.gpa); + self.deferred_numeric_literals.deinit(self.gpa); + self.import_mapping.deinit(); + self.method_idents.deinit(self.gpa); // diagnostics are stored in the NodeStore, no need to free separately self.store.deinit(); + + if (self.evaluation_order) |eval_order| { + eval_order.deinit(); + self.gpa.destroy(eval_order); + } } -/// Freeze all interners in this module environment, preventing any new entries from being added. -/// This should be called after canonicalization is complete, so that -/// we know it's safe to serialize/deserialize the part of the interner -/// that goes from ident to string, because we don't go from string to ident -/// (or add new entries) in any of the later stages of compilation. -pub fn freezeInterners(self: *Self) void { - self.common.freezeInterners(); -} - -// ===== Module compilation functionality ===== +// Module compilation functionality /// Records a diagnostic error during canonicalization without blocking compilation. pub fn pushDiagnostic(self: *Self, reason: CIR.Diagnostic) std.mem.Allocator.Error!void { - _ = try self.addDiagnosticAndTypeVar(reason, .err); + _ = try self.addDiagnostic(reason); } /// Creates a malformed node that represents a runtime error in the IR. pub fn pushMalformed(self: *Self, comptime RetIdx: type, reason: CIR.Diagnostic) std.mem.Allocator.Error!RetIdx { comptime if (!isCastable(RetIdx)) @compileError("Idx type " ++ @typeName(RetIdx) ++ " is not castable"); - const diag_idx = try self.addDiagnosticAndTypeVar(reason, .err); + const diag_idx = try self.addDiagnostic(reason); const region = getDiagnosticRegion(reason); - const malformed_idx = try self.addMalformedAndTypeVar(diag_idx, .err, region); + const malformed_idx = try self.addMalformed(diag_idx, region); return castIdx(Node.Idx, RetIdx, malformed_idx); } @@ -140,11 +628,10 @@ const isCastable = CIR.isCastable; /// Cast function for safely converting between compatible index types pub const castIdx = CIR.castIdx; -// ===== Module compilation functions ===== +// Module compilation functions /// Retrieve all diagnostics collected during canonicalization. pub fn getDiagnostics(self: *Self) std.mem.Allocator.Error![]CIR.Diagnostic { - // Get all diagnostics from the store, not just the ones in self.diagnostics span const all_diagnostics = try self.store.diagnosticSpanFrom(0); const diagnostic_indices = self.store.sliceDiagnostics(all_diagnostics); const diagnostics = try self.gpa.alloc(CIR.Diagnostic, diagnostic_indices.len); @@ -224,6 +711,27 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st break :blk report; }, + .qualified_ident_does_not_exist => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + const ident_name = self.getIdent(data.ident); + + var report = Report.init(allocator, "DOES NOT EXIST", .runtime_error); + const owned_ident = try report.addOwnedString(ident_name); + try report.document.addUnqualifiedSymbol(owned_ident); + try report.document.addReflowingText(" does not exist."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, .exposed_but_not_implemented => |data| blk: { const region_info = self.calcRegionInfo(data.region); @@ -561,7 +1069,6 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st .redundant_exposed => |data| blk: { const ident_name = self.getIdent(data.ident); const region_info = self.calcRegionInfo(data.region); - const original_region_info = self.calcRegionInfo(data.original_region); var report = Report.init(allocator, "REDUNDANT EXPOSED", .warning); const owned_ident = try report.addOwnedString(ident_name); @@ -580,10 +1087,6 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st self.getLineStartsAll(), ); - // we don't need to display the original region info - // as this header is in a single location - _ = original_region_info; - try report.document.addReflowingText("You can remove the duplicate entry to fix this warning."); break :blk report; @@ -625,7 +1128,18 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st try report.document.addAnnotatedText(owned_feature, .emphasized); try report.document.addLineBreak(); try report.document.addLineBreak(); + const owned_filename = try report.addOwnedString(filename); + const region_info = self.calcRegionInfo(data.region); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + try report.document.addLineBreak(); try report.document.addReflowingText("This error doesn't have a proper diagnostic report yet. Let us know if you want to help improve Roc's error messages!"); + try report.document.addLineBreak(); break :blk report; }, .malformed_type_annotation => |data| blk: { @@ -662,6 +1176,16 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st try report.document.addReflowingText(")."); break :blk report; }, + .if_then_not_canonicalized => |_| blk: { + var report = Report.init(allocator, "INVALID IF BRANCH", .runtime_error); + try report.document.addReflowingText("The branch in this "); + try report.document.addKeyword("if"); + try report.document.addReflowingText(" expression could not be processed."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addReflowingText("The branch must contain a valid expression. Check for syntax errors or missing values."); + break :blk report; + }, .if_else_not_canonicalized => |_| blk: { var report = Report.init(allocator, "INVALID IF BRANCH", .runtime_error); try report.document.addReflowingText("The "); @@ -675,12 +1199,33 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st try report.document.addKeyword("else"); try report.document.addReflowingText(" branch must contain a valid expression. Check for syntax errors or missing values."); try report.document.addLineBreak(); - try report.document.addLineBreak(); - try report.document.addReflowingText("Note: Every "); + break :blk report; + }, + .if_expr_without_else => |_| blk: { + var report = Report.init(allocator, "IF EXPRESSION WITHOUT ELSE", .runtime_error); + try report.document.addReflowingText("This "); try report.document.addKeyword("if"); - try report.document.addReflowingText(" expression in Roc must have an "); + try report.document.addReflowingText(" has no "); try report.document.addKeyword("else"); - try report.document.addReflowingText(" branch, and both branches must have the same type."); + try report.document.addReflowingText(" branch, but it's being used as an expression (assigned to a variable, passed to a function, etc.)."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addReflowingText("You can only use "); + try report.document.addKeyword("if"); + try report.document.addReflowingText(" without "); + try report.document.addKeyword("else"); + try report.document.addReflowingText(" when it's a statement. When "); + try report.document.addKeyword("if"); + try report.document.addReflowingText(" is used as an expression that evaluates to a value, "); + try report.document.addKeyword("else"); + try report.document.addReflowingText(" is required because otherwise there wouldn't always be a value available."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addReflowingText("Either add an "); + try report.document.addKeyword("else"); + try report.document.addReflowingText(" branch, or use this "); + try report.document.addKeyword("if"); + try report.document.addReflowingText(" as a standalone statement."); break :blk report; }, .pattern_not_canonicalized => |_| blk: { @@ -688,6 +1233,11 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st try report.document.addReflowingText("This pattern contains invalid syntax or uses unsupported features."); break :blk report; }, + .pattern_arg_invalid => |_| blk: { + var report = Report.init(allocator, "INVALID PATTERN ARGUMENT", .runtime_error); + try report.document.addReflowingText("Pattern arguments must be valid patterns like identifiers, literals, or destructuring patterns."); + break :blk report; + }, .shadowing_warning => |data| blk: { const ident_name = self.getIdent(data.ident); const new_region_info = self.calcRegionInfo(data.region); @@ -749,9 +1299,7 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st break :blk report; }, - .lambda_body_not_canonicalized => |data| blk: { - _ = data; - + .lambda_body_not_canonicalized => blk: { var report = Report.init(allocator, "INVALID LAMBDA", .runtime_error); try report.document.addReflowingText("The body of this lambda expression is not valid."); @@ -777,9 +1325,7 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st break :blk report; }, - .var_across_function_boundary => |data| blk: { - _ = data; - + .var_across_function_boundary => blk: { var report = Report.init(allocator, "VAR REASSIGNMENT ERROR", .runtime_error); try report.document.addReflowingText("Cannot reassign a "); try report.document.addKeyword("var"); @@ -791,9 +1337,7 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st break :blk report; }, - .tuple_elem_not_canonicalized => |data| blk: { - _ = data; - + .tuple_elem_not_canonicalized => blk: { var report = Report.init(allocator, "INVALID TUPLE ELEMENT", .runtime_error); try report.document.addReflowingText("This tuple element is malformed or contains invalid syntax."); @@ -801,7 +1345,7 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st }, .f64_pattern_literal => |data| blk: { // Extract the literal text from the source - const literal_text = self.getSourceAll()[data.region.start.offset..data.region.end.offset]; + const literal_text = self.getSource(data.region); var report = Report.init(allocator, "F64 NOT ALLOWED IN PATTERN", .runtime_error); @@ -852,7 +1396,7 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st // Format the message to match origin/main try report.document.addText("The type "); try report.document.addInlineCode(type_name); - try report.document.addReflowingText(" is not an exposed by the module "); + try report.document.addReflowingText(" is not exposed by the module "); try report.document.addInlineCode(module_name); try report.document.addReflowingText("."); try report.document.addLineBreak(); @@ -871,6 +1415,33 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st break :blk report; }, + .value_not_exposed => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + + var report = Report.init(allocator, "VALUE NOT EXPOSED", .runtime_error); + + // Format the message to match origin/main + try report.document.addText("The value "); + try report.document.addInlineCode(self.getIdent(data.value_name)); + try report.document.addReflowingText(" is not exposed by the module "); + try report.document.addInlineCode(self.getIdent(data.module_name)); + try report.document.addReflowingText("."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addReflowingText("You're attempting to use this value here:"); + try report.document.addLineBreak(); + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, .module_not_found => |data| blk: { const region_info = self.calcRegionInfo(data.region); @@ -927,6 +1498,88 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st break :blk report; }, + .nested_type_not_found => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + + var report = Report.init(allocator, "MISSING NESTED TYPE", .runtime_error); + + const parent_bytes = self.getIdent(data.parent_name); + const parent_name = try report.addOwnedString(parent_bytes); + + const nested_bytes = self.getIdent(data.nested_name); + const nested_name = try report.addOwnedString(nested_bytes); + + try report.document.addInlineCode(parent_name); + try report.document.addReflowingText(" is in scope, but it doesn't have a nested type "); + + if (std.mem.eql(u8, parent_bytes, nested_bytes)) { + // Say "also named" if the parent and nested types are equal, e.g. `Foo.Foo` - when + // this happens it can be kind of a confusing message if the message just says + // "Foo is in scope, but it doesn't have a nested type named Foo" compared to + // "Foo is in scope, but it doesn't have a nested type that's also named Foo" + try report.document.addReflowingText("that's also "); + } + + try report.document.addReflowingText("named "); + try report.document.addInlineCode(nested_name); + try report.document.addReflowingText("."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addReflowingText("It's referenced here:"); + try report.document.addLineBreak(); + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, + .nested_value_not_found => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + + var report = Report.init(allocator, "DOES NOT EXIST", .runtime_error); + + const parent_bytes = self.getIdent(data.parent_name); + const parent_name = try report.addOwnedString(parent_bytes); + + const nested_bytes = self.getIdent(data.nested_name); + const nested_name = try report.addOwnedString(nested_bytes); + + // First line: "Foo.bar does not exist." + const full_name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ parent_bytes, nested_bytes }); + defer allocator.free(full_name); + const owned_full_name = try report.addOwnedString(full_name); + try report.document.addInlineCode(owned_full_name); + try report.document.addReflowingText(" does not exist."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + // Second line: "Foo is in scope, but it has no associated bar." + try report.document.addInlineCode(parent_name); + try report.document.addReflowingText(" is in scope, but it has no associated "); + try report.document.addInlineCode(nested_name); + try report.document.addReflowingText("."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addReflowingText("It's referenced here:"); + try report.document.addLineBreak(); + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, .where_clause_not_allowed_in_type_decl => |data| blk: { const region_info = self.calcRegionInfo(data.region); @@ -952,26 +1605,357 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st break :blk report; }, - else => { - // For unhandled diagnostics, create a generic report - const diagnostic_name = @tagName(diagnostic); + .type_module_missing_matching_type => |data| blk: { + const region_info = self.calcRegionInfo(data.region); - var report = Report.init(allocator, "COMPILER DIAGNOSTIC", .runtime_error); - try report.addHeader("Compiler Diagnostic"); - const message = try std.fmt.allocPrint(allocator, "Diagnostic type '{s}' is not yet handled in report generation.", .{diagnostic_name}); - defer allocator.free(message); - const owned_message = try report.addOwnedString(message); - try report.document.addText(owned_message); + var report = Report.init(allocator, "TYPE MODULE MISSING MATCHING TYPE", .runtime_error); + + const module_name_bytes = self.getIdent(data.module_name); + const module_name = try report.addOwnedString(module_name_bytes); + + try report.document.addReflowingText("Type modules must have a type declaration matching the module name."); + try report.document.addLineBreak(); try report.document.addLineBreak(); - // Add location info even without specific region - const location_msg = try std.fmt.allocPrint(allocator, "**{s}:0:0:0:0**", .{filename}); - defer allocator.free(location_msg); - const owned_location = try report.addOwnedString(location_msg); - try report.document.addText(owned_location); + try report.document.addText("This file is named "); + try report.document.addInlineCode(module_name); + try report.document.addReflowingText(".roc, but no top-level type declaration named "); + try report.document.addInlineCode(module_name); + try report.document.addReflowingText(" was found."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); - return report; + try report.document.addReflowingText("Add either:"); + try report.document.addLineBreak(); + const nominal_msg = try std.fmt.allocPrint(allocator, "{s} := ...", .{module_name_bytes}); + defer allocator.free(nominal_msg); + const owned_nominal = try report.addOwnedString(nominal_msg); + try report.document.addInlineCode(owned_nominal); + try report.document.addReflowingText(" (nominal type)"); + try report.document.addLineBreak(); + try report.document.addReflowingText("or:"); + try report.document.addLineBreak(); + const alias_msg = try std.fmt.allocPrint(allocator, "{s} : ...", .{module_name_bytes}); + defer allocator.free(alias_msg); + const owned_alias = try report.addOwnedString(alias_msg); + try report.document.addInlineCode(owned_alias); + try report.document.addReflowingText(" (type alias)"); + try report.document.addLineBreak(); + + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; }, + .default_app_missing_main => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + + var report = Report.init(allocator, "MISSING MAIN! FUNCTION", .runtime_error); + + try report.document.addReflowingText("Default app modules must have a "); + try report.document.addInlineCode("main!"); + try report.document.addReflowingText(" function."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("No "); + try report.document.addInlineCode("main!"); + try report.document.addReflowingText(" function was found."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addReflowingText("Add a main! function like:"); + try report.document.addLineBreak(); + try report.document.addInlineCode("main! = |arg| { ... }"); + try report.document.addLineBreak(); + + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, + .default_app_wrong_arity => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + + var report = Report.init(allocator, "MAIN! SHOULD TAKE 1 ARGUMENT", .runtime_error); + + try report.document.addInlineCode("main!"); + try report.document.addReflowingText(" is defined but has the wrong number of arguments. "); + try report.document.addInlineCode("main!"); + try report.document.addReflowingText(" should take 1 argument."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + const arity_msg = try std.fmt.allocPrint(allocator, "{d}", .{data.arity}); + defer allocator.free(arity_msg); + const owned_arity = try report.addOwnedString(arity_msg); + try report.document.addText("Found "); + try report.document.addInlineCode(owned_arity); + try report.document.addReflowingText(" arguments."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addReflowingText("Change it to:"); + try report.document.addLineBreak(); + try report.document.addInlineCode("main! = |arg| { ... }"); + try report.document.addLineBreak(); + + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, + .cannot_import_default_app => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + + var report = Report.init(allocator, "CANNOT IMPORT DEFAULT APP", .runtime_error); + + const module_name_bytes = self.getIdent(data.module_name); + const module_name = try report.addOwnedString(module_name_bytes); + + try report.document.addReflowingText("You cannot import a default app module."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("The module "); + try report.document.addInlineCode(module_name); + try report.document.addReflowingText(" is a default app module and cannot be imported."); + try report.document.addLineBreak(); + + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, + .execution_requires_app_or_default_app => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + + var report = Report.init(allocator, "EXECUTION REQUIRES APP OR DEFAULT APP", .runtime_error); + + try report.document.addReflowingText("This file cannot be executed because it is not an app or default-app module."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addReflowingText("Add either:"); + try report.document.addLineBreak(); + try report.document.addInlineCode("app"); + try report.document.addReflowingText(" header at the top of the file"); + try report.document.addLineBreak(); + try report.document.addReflowingText("or:"); + try report.document.addLineBreak(); + try report.document.addReflowingText("a "); + try report.document.addInlineCode("main!"); + try report.document.addReflowingText(" function with 1 argument (for default-app)"); + try report.document.addLineBreak(); + + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, + .type_name_case_mismatch => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + + var report = Report.init(allocator, "TYPE NAME CASE MISMATCH", .runtime_error); + + const module_name_bytes = self.getIdent(data.module_name); + const module_name = try report.addOwnedString(module_name_bytes); + const type_name_bytes = self.getIdent(data.type_name); + const type_name = try report.addOwnedString(type_name_bytes); + + try report.document.addReflowingText("Type module name must match the type declaration."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("This file is named "); + try report.document.addInlineCode(module_name); + try report.document.addReflowingText(".roc, but the type is named "); + try report.document.addInlineCode(type_name); + try report.document.addReflowingText("."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addReflowingText("Make sure the type name matches the filename exactly (case-sensitive)."); + try report.document.addLineBreak(); + + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, + .module_header_deprecated => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + + var report = Report.init(allocator, "MODULE HEADER DEPRECATED", .warning); + + try report.document.addReflowingText("The "); + try report.document.addInlineCode("module"); + try report.document.addReflowingText(" header is deprecated."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addReflowingText("Type modules (headerless files with a top-level type matching the filename) are now the preferred way to define modules."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addReflowingText("Remove the "); + try report.document.addInlineCode("module"); + try report.document.addReflowingText(" header and ensure your file defines a type that matches the filename."); + try report.document.addLineBreak(); + + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .warning_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, + .redundant_expose_main_type => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + + var report = Report.init(allocator, "REDUNDANT EXPOSE", .warning); + + const type_name_bytes = self.getIdent(data.type_name); + const type_name = try report.addOwnedString(type_name_bytes); + const module_name_bytes = self.getIdent(data.module_name); + const module_name = try report.addOwnedString(module_name_bytes); + + try report.document.addReflowingText("Redundantly exposing "); + try report.document.addInlineCode(type_name); + try report.document.addReflowingText(" when importing "); + try report.document.addInlineCode(module_name); + try report.document.addReflowingText("."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addReflowingText("The type "); + try report.document.addInlineCode(type_name); + try report.document.addReflowingText(" is automatically exposed when importing a type module."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addReflowingText("Remove "); + try report.document.addInlineCode(type_name); + try report.document.addReflowingText(" from the exposing clause."); + try report.document.addLineBreak(); + + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .warning_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, + .invalid_main_type_rename_in_exposing => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + + var report = Report.init(allocator, "INVALID TYPE RENAME", .runtime_error); + + const type_name_bytes = self.getIdent(data.type_name); + const type_name = try report.addOwnedString(type_name_bytes); + const alias_bytes = self.getIdent(data.alias); + const alias = try report.addOwnedString(alias_bytes); + + try report.document.addReflowingText("Cannot rename "); + try report.document.addInlineCode(type_name); + try report.document.addReflowingText(" to "); + try report.document.addInlineCode(alias); + try report.document.addReflowingText(" in the exposing clause."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addReflowingText("To rename both the module and its main type, use "); + try report.document.addInlineCode("as"); + try report.document.addReflowingText(" at the module level:"); + try report.document.addLineBreak(); + + const example_msg = try std.fmt.allocPrint(allocator, "import ModuleName as {s}", .{alias_bytes}); + defer allocator.free(example_msg); + const owned_example = try report.addOwnedString(example_msg); + try report.document.addInlineCode(owned_example); + try report.document.addLineBreak(); + + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, + .ident_already_in_scope => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + const ident_name = self.getIdent(data.ident); + + var report = Report.init(allocator, "SHADOWING", .runtime_error); + const owned_ident = try report.addOwnedString(ident_name); + try report.document.addReflowingText("The name "); + try report.document.addUnqualifiedSymbol(owned_ident); + try report.document.addReflowingText(" is already defined in this scope."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addReflowingText("Choose a different name for this identifier."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + const owned_filename = try report.addOwnedString(filename); + try report.document.addSourceRegion( + region_info, + .error_highlight, + owned_filename, + self.getSourceAll(), + self.getLineStartsAll(), + ); + + break :blk report; + }, + else => unreachable, // All diagnostics must have explicit handlers }; } @@ -997,71 +1981,33 @@ pub fn getSourceLine(self: *const Self, region: Region) ![]const u8 { return self.common.getSourceLine(region); } -/// Serialize this ModuleEnv to the given CompactWriter. -/// IMPORTANT: The returned pointer points to memory inside the writer! -/// Attempting to dereference this pointer or calling any methods on it -/// is illegal behavior! -pub fn serialize( - self: *const Self, - allocator: std.mem.Allocator, - writer: *CompactWriter, -) std.mem.Allocator.Error!*const Self { - // First, write the ModuleEnv struct itself - const offset_self = try writer.appendAlloc(allocator, Self); - - // Then serialize the sub-structures and update the struct - offset_self.* = .{ - .gpa = undefined, // Will be set when deserializing - .common = (try self.common.serialize(allocator, writer)).*, - .types = (try self.types.serialize(allocator, writer)).*, - .all_defs = self.all_defs, - .all_statements = self.all_statements, - .external_decls = (try self.external_decls.serialize(allocator, writer)).*, - .imports = (try self.imports.serialize(allocator, writer)).*, - .module_name = "", // Will be set when deserializing - .diagnostics = self.diagnostics, - .store = (try self.store.serialize(allocator, writer)).*, - }; - - // set gpa to all zeros, so that what we write to the file is deterministic - @memset(@as([*]u8, @ptrCast(&offset_self.gpa))[0..@sizeOf(@TypeOf(offset_self.gpa))], 0); - - return @constCast(offset_self); -} - -/// Add the given offset to the memory addresses of all pointers in `self`. -/// IMPORTANT: The gpa, source, and module_name fields must be manually set before calling this function. -pub fn relocate(self: *Self, offset: isize) void { - // IMPORTANT: gpa, and module_name are not relocated - they should be set manually before calling relocate - - // Relocate all sub-structures - self.common.relocate(offset); - self.types.relocate(offset); - - // Note: all_defs and all_statements are just spans with numeric values, no pointers to relocate - - self.external_decls.relocate(offset); - // self.imports is deserialized separately, so no need to relocate here - - // Note: module_name is not relocated - it should be set manually - - // Note: diagnostics is just a span with numeric values, no pointers to relocate - - self.store.relocate(offset); -} - -/// Serialized representation of ModuleEnv -pub const Serialized = struct { - gpa: std.mem.Allocator, // Serialized as zeros, provided during deserialization +/// Serialized representation of ModuleEnv. +/// Uses extern struct to guarantee consistent field layout across optimization levels. +pub const Serialized = extern struct { + // Field order must match the runtime ModuleEnv struct exactly for in-place deserialization + gpa: [2]u64, // Reserve space for allocator (vtable ptr + context ptr), provided during deserialization common: CommonEnv.Serialized, types: TypeStore.Serialized, + module_kind: ModuleKind.Serialized, all_defs: CIR.Def.Span, all_statements: CIR.Statement.Span, + exports: CIR.Def.Span, + requires_types: RequiredType.SafeList.Serialized, + for_clause_aliases: ForClauseAlias.SafeList.Serialized, + rigid_vars_reserved: [4]u64, // Reserved space for rigid_vars (AutoHashMapUnmanaged is ~32 bytes), initialized at runtime + builtin_statements: CIR.Statement.Span, external_decls: CIR.ExternalDecl.SafeList.Serialized, imports: CIR.Import.Store.Serialized, - module_name: []const u8, // Serialized as zeros, provided during deserialization + module_name: [2]u64, // Reserve space for slice (ptr + len), provided during deserialization + module_name_idx_reserved: u32, // Reserved space for module_name_idx field (interned during deserialization) diagnostics: CIR.Diagnostic.Span, store: NodeStore.Serialized, + evaluation_order_reserved: u64, // Reserved space for evaluation_order field (required for in-place deserialization cast) + // Well-known identifier indices (serialized directly, no lookup needed during deserialization) + idents: CommonIdents, + deferred_numeric_literals: DeferredNumericLiteral.SafeList.Serialized, + import_mapping_reserved: [6]u64, // Reserved space for import_mapping (AutoHashMap is ~40 bytes), initialized at runtime + method_idents: MethodIdents.Serialized, /// Serialize a ModuleEnv into this Serialized struct, appending data to the writer pub fn serialize( @@ -1070,16 +2016,18 @@ pub const Serialized = struct { allocator: std.mem.Allocator, writer: *CompactWriter, ) !void { - // Set fields that will be provided during deserialization to zeros - self.gpa = undefined; // Will be set to zeros below - try self.common.serialize(&env.common, allocator, writer); try self.types.serialize(&env.types, allocator, writer); // Copy simple values directly + self.module_kind = ModuleKind.Serialized.encode(env.module_kind); self.all_defs = env.all_defs; self.all_statements = env.all_statements; + self.exports = env.exports; + self.builtin_statements = env.builtin_statements; + try self.requires_types.serialize(&env.requires_types, allocator, writer); + try self.for_clause_aliases.serialize(&env.for_clause_aliases, allocator, writer); try self.external_decls.serialize(&env.external_decls, allocator, writer); try self.imports.serialize(&env.imports, allocator, writer); @@ -1088,9 +2036,24 @@ pub const Serialized = struct { // Serialize NodeStore try self.store.serialize(&env.store, allocator, writer); - // Set gpa to all zeros; the space needs to be here, - // but the value will be set separately during deserialization. - @memset(@as([*]u8, @ptrCast(&self.gpa))[0..@sizeOf(@TypeOf(self.gpa))], 0); + // Serialize deferred numeric literals (will be empty during serialization since it's only used during type checking/evaluation) + try self.deferred_numeric_literals.serialize(&env.deferred_numeric_literals, allocator, writer); + + // Set gpa, module_name, module_name_idx_reserved, evaluation_order_reserved to zeros; + // these are runtime-only and will be set during deserialization. + self.gpa = .{ 0, 0 }; + self.module_name = .{ 0, 0 }; + self.module_name_idx_reserved = 0; + self.evaluation_order_reserved = 0; + // rigid_vars is runtime-only and initialized fresh during deserialization + self.rigid_vars_reserved = .{ 0, 0, 0, 0 }; + + // Serialize well-known identifier indices directly (no lookup needed during deserialization) + self.idents = env.idents; + // import_mapping is runtime-only and initialized fresh during deserialization + self.import_mapping_reserved = .{ 0, 0, 0, 0, 0, 0 }; + // Serialize method_idents map + try self.method_idents.serialize(&env.method_idents, allocator, writer); } /// Deserialize a ModuleEnv from the buffer, updating the ModuleEnv in place @@ -1100,24 +2063,46 @@ pub const Serialized = struct { gpa: std.mem.Allocator, source: []const u8, module_name: []const u8, - ) *Self { - // ModuleEnv.Serialized should be at least as big as ModuleEnv - std.debug.assert(@sizeOf(Serialized) >= @sizeOf(Self)); + ) std.mem.Allocator.Error!*Self { + // Verify that Serialized is at least as large as the runtime struct. + // This is required because we're reusing the same memory location. + // On 32-bit platforms, Serialized may be larger due to using fixed-size types for platform-independent serialization. + // In Debug builds, Self may be larger due to debug-only store tracking fields, so skip this check. + comptime { + if (builtin.mode != .Debug) { + std.debug.assert(@sizeOf(@This()) >= @sizeOf(Self)); + } + } // Overwrite ourself with the deserialized version, and return our pointer after casting it to Self. const env = @as(*Self, @ptrFromInt(@intFromPtr(self))); + // Deserialize common env first so we can look up identifiers + const common = self.common.deserialize(offset, source).*; + env.* = Self{ .gpa = gpa, - .common = self.common.deserialize(offset, source).*, - .types = self.types.deserialize(offset).*, + .common = common, + .types = self.types.deserialize(offset, gpa).*, + .module_kind = self.module_kind.decode(), .all_defs = self.all_defs, .all_statements = self.all_statements, + .exports = self.exports, + .requires_types = self.requires_types.deserialize(offset).*, + .for_clause_aliases = self.for_clause_aliases.deserialize(offset).*, + .builtin_statements = self.builtin_statements, .external_decls = self.external_decls.deserialize(offset).*, - .imports = self.imports.deserialize(offset, gpa).*, + .imports = (try self.imports.deserialize(offset, gpa)).*, .module_name = module_name, + .module_name_idx = Ident.Idx.NONE, // Not used for deserialized modules (only needed during fresh canonicalization) .diagnostics = self.diagnostics, .store = self.store.deserialize(offset, gpa).*, + .evaluation_order = null, // Not serialized, will be recomputed if needed + .idents = self.idents, + .deferred_numeric_literals = self.deferred_numeric_literals.deserialize(offset).*, + .import_mapping = types_mod.import_mapping.ImportMapping.init(gpa), + .method_idents = self.method_idents.deserialize(offset).*, + .rigid_vars = std.AutoHashMapUnmanaged(Ident.Idx, TypeVar){}, }; return env; @@ -1149,6 +2134,19 @@ pub fn getExposedNodeIndexById(self: *const Self, ident_idx: Ident.Idx) ?u16 { return self.common.getNodeIndexById(self.gpa, ident_idx); } +/// Get the exposed node index for a type given its statement index. +/// This is used for auto-imported builtin types where we have the statement index pre-computed. +/// For auto-imported types, the statement index IS the node/var index directly. +pub fn getExposedNodeIndexByStatementIdx(self: *const Self, stmt_idx: CIR.Statement.Idx) ?u16 { + _ = self; // Not needed for this simplified implementation + + // For auto-imported builtin types (Bool, Try, etc.), the statement index + // IS the node/var index. This is because type declarations get type variables + // indexed by their statement index, not by their position in arrays. + const node_idx: u16 = @intCast(@intFromEnum(stmt_idx)); + return node_idx; +} + /// Ensures that the exposed items are sorted by identifier index. pub fn ensureExposedSorted(self: *Self, allocator: std.mem.Allocator) void { self.common.exposed_items.ensureSorted(allocator); @@ -1159,17 +2157,16 @@ pub fn containsExposedById(self: *const Self, ident_idx: Ident.Idx) bool { return self.common.exposed_items.containsById(self.gpa, @bitCast(ident_idx)); } -/// Assert that nodes, regions and types are all in sync +/// Assert that nodes and regions are in sync pub inline fn debugAssertArraysInSync(self: *const Self) void { if (builtin.mode == .Debug) { const cir_nodes = self.store.nodes.items.len; const region_nodes = self.store.regions.len(); - const type_nodes = self.types.len(); - if (!(cir_nodes == region_nodes and region_nodes == type_nodes)) { + if (!(cir_nodes == region_nodes)) { std.debug.panic( - "Arrays out of sync:\n cir_nodes={}\n region_nodes={}\n type_nodes={}\n", - .{ cir_nodes, region_nodes, type_nodes }, + "Arrays out of sync:\n cir_nodes={}\n region_nodes={}\n", + .{ cir_nodes, region_nodes }, ); } } @@ -1190,311 +2187,164 @@ inline fn debugAssertIdxsEql(comptime desc: []const u8, idx1: anytype, idx2: any } } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addDefAndTypeVar(self: *Self, expr: CIR.Def, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.Def.Idx { +/// Add a new expression to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addDef(self: *Self, expr: CIR.Def, region: Region) std.mem.Allocator.Error!CIR.Def.Idx { const expr_idx = try self.store.addDef(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("self", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addTypeHeaderAndTypeVar(self: *Self, expr: CIR.TypeHeader, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.TypeHeader.Idx { +/// Add a new type header to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addTypeHeader(self: *Self, expr: CIR.TypeHeader, region: Region) std.mem.Allocator.Error!CIR.TypeHeader.Idx { const expr_idx = try self.store.addTypeHeader(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addTypeHeaderAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addStatementAndTypeVar(self: *Self, expr: CIR.Statement, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.Statement.Idx { +/// Add a new statement to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addStatement(self: *Self, expr: CIR.Statement, region: Region) std.mem.Allocator.Error!CIR.Statement.Idx { const expr_idx = try self.store.addStatement(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addStatementAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addStatementAndTypeVarRedirect(self: *Self, expr: CIR.Statement, redirect_to: TypeVar, region: Region) std.mem.Allocator.Error!CIR.Statement.Idx { - const expr_idx = try self.store.addStatement(expr, region); - const expr_var = try self.types.freshRedirect(redirect_to); - debugAssertIdxsEql("addStatementAndTypeVarRedirect", expr_idx, expr_var); - self.debugAssertArraysInSync(); - return expr_idx; -} -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addPatternAndTypeVar(self: *Self, expr: CIR.Pattern, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.Pattern.Idx { +/// Add a new pattern to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addPattern(self: *Self, expr: CIR.Pattern, region: Region) std.mem.Allocator.Error!CIR.Pattern.Idx { const expr_idx = try self.store.addPattern(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addPatternAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addPatternAndTypeVarRedirect(self: *Self, expr: CIR.Pattern, redirect_to: TypeVar, region: Region) std.mem.Allocator.Error!CIR.Pattern.Idx { - const expr_idx = try self.store.addPattern(expr, region); - const expr_var = try self.types.freshRedirect(redirect_to); - debugAssertIdxsEql("addPatternAndTypeVar", expr_idx, expr_var); - self.debugAssertArraysInSync(); - return expr_idx; -} - -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addExprAndTypeVar(self: *Self, expr: CIR.Expr, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.Expr.Idx { +/// Add a new expression to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addExpr(self: *Self, expr: CIR.Expr, region: Region) std.mem.Allocator.Error!CIR.Expr.Idx { const expr_idx = try self.store.addExpr(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addExprAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addExprAndTypeVarRedirect(self: *Self, expr: CIR.Expr, redirect_to: TypeVar, region: Region) std.mem.Allocator.Error!CIR.Expr.Idx { - const expr_idx = try self.store.addExpr(expr, region); - const expr_var = try self.types.freshRedirect(redirect_to); - debugAssertIdxsEql("addExprAndTypeVarRedirect", expr_idx, expr_var); - self.debugAssertArraysInSync(); - return expr_idx; -} - -/// Add a new capture and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addCaptureAndTypeVar(self: *Self, capture: CIR.Expr.Capture, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.Expr.Capture.Idx { +/// Add a new capture to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addCapture(self: *Self, capture: CIR.Expr.Capture, region: Region) std.mem.Allocator.Error!CIR.Expr.Capture.Idx { const capture_idx = try self.store.addCapture(capture, region); - const capture_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addCaptureAndTypeVar", capture_idx, capture_var); self.debugAssertArraysInSync(); return capture_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addRecordFieldAndTypeVar(self: *Self, expr: CIR.RecordField, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.RecordField.Idx { +/// Add a new record field to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addRecordField(self: *Self, expr: CIR.RecordField, region: Region) std.mem.Allocator.Error!CIR.RecordField.Idx { const expr_idx = try self.store.addRecordField(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addRecordFieldAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addRecordDestructAndTypeVar(self: *Self, expr: CIR.Pattern.RecordDestruct, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.Pattern.RecordDestruct.Idx { +/// Add a new record destructuring to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addRecordDestruct(self: *Self, expr: CIR.Pattern.RecordDestruct, region: Region) std.mem.Allocator.Error!CIR.Pattern.RecordDestruct.Idx { const expr_idx = try self.store.addRecordDestruct(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addRecordDestructorAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addIfBranchAndTypeVar(self: *Self, expr: CIR.Expr.IfBranch, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.Expr.IfBranch.Idx { +/// Adds a new if branch to the store. +/// This function asserts that the nodes and regions are in sync. +pub fn addIfBranch(self: *Self, expr: CIR.Expr.IfBranch, region: Region) std.mem.Allocator.Error!CIR.Expr.IfBranch.Idx { const expr_idx = try self.store.addIfBranch(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addIfBranchAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addMatchBranchAndTypeVar(self: *Self, expr: CIR.Expr.Match.Branch, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.Expr.Match.Branch.Idx { +/// Add a new match branch to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addMatchBranch(self: *Self, expr: CIR.Expr.Match.Branch, region: Region) std.mem.Allocator.Error!CIR.Expr.Match.Branch.Idx { const expr_idx = try self.store.addMatchBranch(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addMatchBranchAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addWhereClauseAndTypeVar(self: *Self, expr: CIR.WhereClause, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.WhereClause.Idx { +/// Add a new where clause to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addWhereClause(self: *Self, expr: CIR.WhereClause, region: Region) std.mem.Allocator.Error!CIR.WhereClause.Idx { const expr_idx = try self.store.addWhereClause(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addWhereClauseAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addTypeAnnoAndTypeVar(self: *Self, expr: CIR.TypeAnno, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.TypeAnno.Idx { +/// Add a new type annotation to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addTypeAnno(self: *Self, expr: CIR.TypeAnno, region: Region) std.mem.Allocator.Error!CIR.TypeAnno.Idx { const expr_idx = try self.store.addTypeAnno(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addTypeAnnoAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addTypeAnnoAndTypeVarRedirect(self: *Self, expr: CIR.TypeAnno, redirect_to: TypeVar, region: Region) std.mem.Allocator.Error!CIR.TypeAnno.Idx { - const expr_idx = try self.store.addTypeAnno(expr, region); - const expr_var = try self.types.freshRedirect(redirect_to); - debugAssertIdxsEql("addTypeAnnoAndTypeVarRedirect", expr_idx, expr_var); - self.debugAssertArraysInSync(); - return expr_idx; -} - -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addAnnotationAndTypeVar(self: *Self, expr: CIR.Annotation, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.Annotation.Idx { +/// Add a new annotation to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addAnnotation(self: *Self, expr: CIR.Annotation, region: Region) std.mem.Allocator.Error!CIR.Annotation.Idx { const expr_idx = try self.store.addAnnotation(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addAnnotationAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addAnnotationAndTypeVarRedirect(self: *Self, expr: CIR.Annotation, redirect_to: TypeVar, region: Region) std.mem.Allocator.Error!CIR.Annotation.Idx { - const expr_idx = try self.store.addAnnotation(expr, region); - const expr_var = try self.types.freshRedirect(redirect_to); - debugAssertIdxsEql("addAnnotationAndTypeVar", expr_idx, expr_var); - self.debugAssertArraysInSync(); - return expr_idx; -} - -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addAnnoRecordFieldAndTypeVar(self: *Self, expr: CIR.TypeAnno.RecordField, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.TypeAnno.RecordField.Idx { +/// Add a new record field to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addAnnoRecordField(self: *Self, expr: CIR.TypeAnno.RecordField, region: Region) std.mem.Allocator.Error!CIR.TypeAnno.RecordField.Idx { const expr_idx = try self.store.addAnnoRecordField(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addAnnoRecordFieldAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addAnnoRecordFieldAndTypeVarRedirect(self: *Self, expr: CIR.TypeAnno.RecordField, redirect_to: TypeVar, region: Region) std.mem.Allocator.Error!CIR.TypeAnno.RecordField.Idx { - const expr_idx = try self.store.addAnnoRecordField(expr, region); - const expr_var = try self.types.freshRedirect(redirect_to); - debugAssertIdxsEql("addAnnoRecordFieldAndTypeVar", expr_idx, expr_var); - self.debugAssertArraysInSync(); - return expr_idx; -} - -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addExposedItemAndTypeVar(self: *Self, expr: CIR.ExposedItem, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.ExposedItem.Idx { +/// Add a new exposed item to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addExposedItem(self: *Self, expr: CIR.ExposedItem, region: Region) std.mem.Allocator.Error!CIR.ExposedItem.Idx { const expr_idx = try self.store.addExposedItem(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addExposedItemAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a diagnostic without creating a corresponding type variable. +/// Add a diagnostic. +/// This function asserts that the nodes and regions are in sync. pub fn addDiagnostic(self: *Self, reason: CIR.Diagnostic) std.mem.Allocator.Error!CIR.Diagnostic.Idx { - return self.store.addDiagnostic(reason); -} - -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addDiagnosticAndTypeVar(self: *Self, reason: CIR.Diagnostic, content: types_mod.Content) std.mem.Allocator.Error!CIR.Diagnostic.Idx { const expr_idx = try self.store.addDiagnostic(reason); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addDiagnosticAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addMalformedAndTypeVar(self: *Self, diagnostic_idx: CIR.Diagnostic.Idx, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.Node.Idx { +/// Add a new malformed node to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addMalformed(self: *Self, diagnostic_idx: CIR.Diagnostic.Idx, region: Region) std.mem.Allocator.Error!CIR.Node.Idx { const malformed_idx = try self.store.addMalformed(diagnostic_idx, region); - const malformed_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addMalformedAndTypeVar", malformed_idx, malformed_var); self.debugAssertArraysInSync(); return malformed_idx; } -/// Add a new match branch pattern and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addMatchBranchPatternAndTypeVar(self: *Self, expr: CIR.Expr.Match.BranchPattern, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.Expr.Match.BranchPattern.Idx { +/// Add a new match branch pattern to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addMatchBranchPattern(self: *Self, expr: CIR.Expr.Match.BranchPattern, region: Region) std.mem.Allocator.Error!CIR.Expr.Match.BranchPattern.Idx { const expr_idx = try self.store.addMatchBranchPattern(expr, region); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addMatchBranchPatternAndTypeVar", expr_idx, expr_var); self.debugAssertArraysInSync(); return expr_idx; } -/// Add a new pattern record field and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addPatternRecordFieldAndTypeVar(self: *Self, expr: CIR.PatternRecordField, content: types_mod.Content, region: Region) std.mem.Allocator.Error!CIR.PatternRecordField.Idx { - _ = region; - const expr_idx = try self.store.addPatternRecordField(expr); - const expr_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addPatternRecordFieldAndTypeVar", expr_idx, expr_var); - self.debugAssertArraysInSync(); - return expr_idx; -} - -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addTypeSlotAndTypeVar( +/// Add a new type variable to the node store. +/// This function asserts that the nodes and regions are in sync. +pub fn addTypeSlot( self: *Self, parent_node: CIR.Node.Idx, - content: types_mod.Content, region: Region, comptime RetIdx: type, ) std.mem.Allocator.Error!RetIdx { comptime if (!isCastable(RetIdx)) @compileError("Idx type " ++ @typeName(RetIdx) ++ " is not castable"); const node_idx = try self.store.addTypeVarSlot(parent_node, region); - const node_var = try self.types.freshFromContent(content); - debugAssertIdxsEql("addTypeSlotAndTypeVar", node_idx, node_var); self.debugAssertArraysInSync(); return @enumFromInt(@intFromEnum(node_idx)); } -/// Add a new expression and type variable. -/// This function asserts that the types array and the nodes are in sync. -pub fn addTypeSlotAndTypeVarRedirect( - self: *Self, - parent_node: CIR.Node.Idx, - redirect_to: TypeVar, - region: Region, - comptime RetIdx: type, -) std.mem.Allocator.Error!RetIdx { - comptime if (!isCastable(RetIdx)) @compileError("Idx type " ++ @typeName(RetIdx) ++ " is not castable"); - const node_idx = try self.store.addTypeVarSlot(parent_node, region); - const node_var = try self.types.freshRedirect(redirect_to); - debugAssertIdxsEql("addTypeSlotAndTypeVarRedirect", node_idx, node_var); - self.debugAssertArraysInSync(); - return @enumFromInt(@intFromEnum(node_idx)); -} - -/// Function that redirects an existing node to the provided var. -/// Assert that the requested idx in in bounds -pub fn redirectTypeTo( - self: *Self, - comptime FromIdx: type, - at_idx: FromIdx, - redirect_to: types_mod.Var, -) std.mem.Allocator.Error!void { - comptime if (!isCastable(FromIdx)) @compileError("Idx type " ++ @typeName(FromIdx) ++ " is not castable"); - self.debugAssertArraysInSync(); - std.debug.assert(@intFromEnum(at_idx) < self.types.len()); - - const var_ = varFrom(at_idx); - try self.types.setVarRedirect(var_, redirect_to); -} - /// Adds an external declaration and returns its index pub fn pushExternalDecl(self: *Self, decl: CIR.ExternalDecl) std.mem.Allocator.Error!CIR.ExternalDecl.Idx { const idx = @as(u32, @intCast(self.external_decls.len())); @@ -1604,6 +2454,10 @@ pub fn pushTypesToSExprTree(self: *Self, maybe_expr_idx: ?CIR.Expr.Idx, tree: *S if (maybe_expr_idx) |expr_idx| { try self.pushExprTypesToSExprTree(expr_idx, tree); } else { + // Create a TypeWriter to format the type + var type_writer = try self.initTypeWriter(); + defer type_writer.deinit(); + // Generate full type information for all definitions and expressions const root_begin = tree.beginNode(); try tree.pushStaticAtom("inferred-types"); @@ -1633,12 +2487,8 @@ pub fn pushTypesToSExprTree(self: *Self, maybe_expr_idx: ?CIR.Expr.Idx, tree: *S const pattern_node_idx: CIR.Node.Idx = @enumFromInt(@intFromEnum(def.pattern)); const pattern_region = self.store.getRegionAt(pattern_node_idx); - // Create a TypeWriter to format the type - var type_writer = self.initTypeWriter() catch continue; - defer type_writer.deinit(); - // Write the type to the buffer - type_writer.write(pattern_var) catch continue; + try type_writer.write(pattern_var, .one_line); // Add the pattern type entry const patt_begin = tree.beginNode(); @@ -1687,12 +2537,8 @@ pub fn pushTypesToSExprTree(self: *Self, maybe_expr_idx: ?CIR.Expr.Idx, tree: *S // Get the type variable for this statement const stmt_var = varFrom(stmt_idx); - // Create a TypeWriter to format the type - var type_writer = self.initTypeWriter() catch continue; - defer type_writer.deinit(); - // Write the type to the buffer - type_writer.write(stmt_var) catch continue; + try type_writer.write(stmt_var, .one_line); const type_str = type_writer.get(); try tree.pushStringPair("type", type_str); @@ -1716,12 +2562,8 @@ pub fn pushTypesToSExprTree(self: *Self, maybe_expr_idx: ?CIR.Expr.Idx, tree: *S // Get the type variable for this statement const stmt_var = varFrom(stmt_idx); - // Create a TypeWriter to format the type - var type_writer = self.initTypeWriter() catch continue; - defer type_writer.deinit(); - // Write the type to the buffer - type_writer.write(stmt_var) catch continue; + try type_writer.write(stmt_var, .one_line); const type_str = type_writer.get(); try tree.pushStringPair("type", type_str); @@ -1756,11 +2598,8 @@ pub fn pushTypesToSExprTree(self: *Self, maybe_expr_idx: ?CIR.Expr.Idx, tree: *S const expr_region = self.store.getRegionAt(expr_node_idx); // Create a TypeWriter to format the type - var type_writer = self.initTypeWriter() catch continue; - defer type_writer.deinit(); - // Write the type to the buffer - type_writer.write(expr_var) catch continue; + try type_writer.write(expr_var, .one_line); // Add the expression type entry const expr_begin = tree.beginNode(); @@ -1793,7 +2632,7 @@ fn pushExprTypesToSExprTree(self: *Self, expr_idx: CIR.Expr.Idx, tree: *SExprTre defer type_writer.deinit(); // Write the type to the buffer - try type_writer.write(expr_var); + try type_writer.write(expr_var, .one_line); // Add the formatted type to the S-expression tree const type_str = type_writer.get(); @@ -1817,6 +2656,11 @@ pub fn getIdentStore(self: *Self) *Ident.Store { return &self.common.idents; } +/// Returns an immutable reference to the identifier store. +pub fn getIdentStoreConst(self: *const Self) *const Ident.Store { + return &self.common.idents; +} + /// Retrieves the text of an identifier by its index. pub fn getIdent(self: *const Self, idx: Ident.Idx) []const u8 { return self.common.getIdent(idx); @@ -1827,21 +2671,24 @@ pub fn getSource(self: *const Self, region: Region) []const u8 { return self.common.getSource(region); } -/// TODO this is a code smell... we should track down the places using this -/// and replace with something more sensible -- need to refactor diagnostics a little. +/// Get the entire source text. This is primarily needed for diagnostic output +/// where `addSourceRegion` requires access to the full source and line starts +/// to render error messages with context lines. +/// +/// For extracting source text for a specific region, prefer `getSource(region)` instead. pub fn getSourceAll(self: *const Self) []const u8 { return self.common.getSourceAll(); } -/// TODO this is a code smell... we should track down the places using this -/// and replace with something more sensible -- need to refactor diagnostics a little. +/// Get all line start offsets. This is primarily needed for diagnostic output +/// where `addSourceRegion` requires access to the full source and line starts +/// to render error messages with context lines. pub fn getLineStartsAll(self: *const Self) []const u32 { return self.common.getLineStartsAll(); } -/// Initialize a TypeWriter with an immutable ModuleEnv reference. pub fn initTypeWriter(self: *Self) std.mem.Allocator.Error!TypeWriter { - return TypeWriter.initFromParts(self.gpa, &self.types, self.getIdentStore()); + return TypeWriter.initFromParts(self.gpa, &self.types, self.getIdentStore(), null); } /// Inserts an identifier into the common environment and returns its index. @@ -1849,6 +2696,213 @@ pub fn insertIdent(self: *Self, ident: Ident) std.mem.Allocator.Error!Ident.Idx return try self.common.insertIdent(self.gpa, ident); } +/// Creates and inserts a qualified identifier (e.g., "Foo.bar") into the common environment. +/// This handles the full lifecycle: building the qualified name, creating the Ident, +/// inserting it into the store, and cleaning up any temporary allocations. +/// All memory management is handled internally with no caller obligations. +pub fn insertQualifiedIdent( + self: *Self, + parent: []const u8, + child: []const u8, +) std.mem.Allocator.Error!Ident.Idx { + const total_len = parent.len + 1 + child.len; // parent + '.' + child + + if (total_len <= 256) { + // Use stack buffer for small identifiers + var buf: [256]u8 = undefined; + const qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ parent, child }) catch unreachable; + return try self.insertIdent(Ident.for_text(qualified)); + } else { + // Use heap allocation for large identifiers + const qualified = try std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ parent, child }); + defer self.gpa.free(qualified); + return try self.insertIdent(Ident.for_text(qualified)); + } +} + +/// Looks up a method identifier on a type by building the qualified method name. +/// This handles cross-module method lookup by building names like "Builtin.Num.U64.from_numeral". +/// +/// Parameters: +/// - type_name: The type's identifier text (e.g., "Num.U64" or "Bool") +/// - method_name: The unqualified method name (e.g., "from_numeral") +/// +/// Returns the method's ident index if found, or null if the method doesn't exist. +/// This is a read-only operation that doesn't modify the ident store. +pub fn getMethodIdent(self: *const Self, type_name: []const u8, method_name: []const u8) ?Ident.Idx { + // Build the qualified method name: "{type_name}.{method_name}" + // The type_name may already include the module prefix (e.g., "Num.U64") + // or just be the type name (e.g., "Bool" for Builtin.Bool) + const total_len = self.module_name.len + 1 + type_name.len + 1 + method_name.len; + + if (total_len <= 256) { + // Use stack buffer for small identifiers + var buf: [256]u8 = undefined; + + // Check if type_name already starts with module_name + if (type_name.len > self.module_name.len and + std.mem.startsWith(u8, type_name, self.module_name) and + type_name[self.module_name.len] == '.') + { + // Type name is already qualified (e.g., "Builtin.Bool") + const qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ type_name, method_name }) catch return null; + return self.getIdentStoreConst().findByString(qualified); + } else if (std.mem.eql(u8, type_name, self.module_name)) { + // Type name IS the module name (e.g., looking up method on "Builtin" itself) + const qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ type_name, method_name }) catch return null; + return self.getIdentStoreConst().findByString(qualified); + } else { + // Try module-qualified name first (e.g., "Builtin.Num.U64.from_numeral") + const qualified = std.fmt.bufPrint(&buf, "{s}.{s}.{s}", .{ self.module_name, type_name, method_name }) catch return null; + if (self.getIdentStoreConst().findByString(qualified)) |idx| { + return idx; + } + // Fallback: try without module prefix (e.g., "Color.as_str" for app-defined types) + // This handles the case where methods are registered with just the type-qualified name + const simple_qualified = std.fmt.bufPrint(&buf, "{s}.{s}", .{ type_name, method_name }) catch return null; + return self.getIdentStoreConst().findByString(simple_qualified); + } + } else { + // Use heap allocation for large identifiers (rare case) + // Try module-qualified name first + const qualified = if (type_name.len > self.module_name.len and + std.mem.startsWith(u8, type_name, self.module_name) and + type_name[self.module_name.len] == '.') + std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ type_name, method_name }) catch return null + else if (std.mem.eql(u8, type_name, self.module_name)) + std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ type_name, method_name }) catch return null + else + std.fmt.allocPrint(self.gpa, "{s}.{s}.{s}", .{ self.module_name, type_name, method_name }) catch return null; + defer self.gpa.free(qualified); + if (self.getIdentStoreConst().findByString(qualified)) |idx| { + return idx; + } + // Fallback for the module-qualified case + if (type_name.len <= self.module_name.len or + !std.mem.startsWith(u8, type_name, self.module_name) or + type_name[self.module_name.len] != '.') + { + const simple_qualified = std.fmt.allocPrint(self.gpa, "{s}.{s}", .{ type_name, method_name }) catch return null; + defer self.gpa.free(simple_qualified); + return self.getIdentStoreConst().findByString(simple_qualified); + } + return null; + } +} + +/// Registers a method identifier mapping for fast index-based lookup. +/// This should be called during canonicalization when a method is defined in an associated block. +/// +/// Parameters: +/// - type_ident: The type's identifier index (e.g., the ident for "Bool") +/// - method_ident: The method's identifier index (e.g., the ident for "is_eq") +/// - qualified_ident: The qualified method ident (e.g., "Bool.is_eq") +pub fn registerMethodIdent(self: *Self, type_ident: Ident.Idx, method_ident: Ident.Idx, qualified_ident: Ident.Idx) !void { + const key = MethodKey{ .type_ident = type_ident, .method_ident = method_ident }; + try self.method_idents.put(self.gpa, key, qualified_ident); +} + +/// Looks up a method identifier by type and method ident indices. +/// This is the fast O(log n) index-based lookup that avoids string comparison. +/// +/// Parameters: +/// - type_ident: The type's identifier index (must be in this module's ident store) +/// - method_ident: The method's identifier index (must be in this module's ident store) +/// +/// Returns the qualified method's ident index if found, or null if not registered. +pub fn lookupMethodIdent(self: *Self, type_ident: Ident.Idx, method_ident: Ident.Idx) ?Ident.Idx { + const key = MethodKey{ .type_ident = type_ident, .method_ident = method_ident }; + return self.method_idents.get(self.gpa, key); +} + +/// Looks up a method identifier by type and method ident indices (const version). +/// This is the fast O(log n) index-based lookup that avoids string comparison. +pub fn lookupMethodIdentConst(self: *const Self, type_ident: Ident.Idx, method_ident: Ident.Idx) ?Ident.Idx { + const key = MethodKey{ .type_ident = type_ident, .method_ident = method_ident }; + // Cast away const for the get operation (it doesn't modify the structure, just ensures sorted) + const mutable_self = @constCast(self); + return mutable_self.method_idents.get(self.gpa, key); +} + +/// Looks up a method identifier by translating idents from a source environment. +/// This first finds the corresponding idents in this module, then does index-based lookup. +/// +/// Parameters: +/// - source_env: The module environment where type_ident and method_ident are from +/// - type_ident: The type's identifier index in source_env +/// - method_ident: The method's identifier index in source_env +/// +/// Returns the qualified method's ident index if found, or null if the method doesn't exist. +/// Falls back to string-based getMethodIdent for backward compatibility with pre-compiled modules. +pub fn lookupMethodIdentFromEnv(self: *Self, source_env: *const Self, type_ident: Ident.Idx, method_ident: Ident.Idx) ?Ident.Idx { + // First, try to find the type and method idents in our own ident store + const type_name = source_env.getIdent(type_ident); + const method_name = source_env.getIdent(method_ident); + + // Find corresponding idents in this module + const local_type_ident = self.common.findIdent(type_name) orelse return null; + const local_method_ident = self.common.findIdent(method_name) orelse return null; + + // Try index-based lookup first (O(log n)) + if (self.lookupMethodIdent(local_type_ident, local_method_ident)) |result| { + return result; + } + + // Fall back to string-based lookup for backward compatibility with pre-compiled modules + // that don't have method_idents populated. This can be removed once all modules are recompiled. + return self.getMethodIdent(type_name, method_name); +} + +/// Const version of lookupMethodIdentFromEnv for use with immutable module environments. +/// Safe to use on deserialized modules since method_idents is already sorted. +/// Falls back to string-based getMethodIdent for backward compatibility with pre-compiled modules. +pub fn lookupMethodIdentFromEnvConst(self: *const Self, source_env: *const Self, type_ident: Ident.Idx, method_ident: Ident.Idx) ?Ident.Idx { + // First, try to find the type and method idents in our own ident store + const type_name = source_env.getIdent(type_ident); + const method_name = source_env.getIdent(method_ident); + + // Find corresponding idents in this module + const local_type_ident = self.common.findIdent(type_name) orelse return null; + const local_method_ident = self.common.findIdent(method_name) orelse return null; + + // Try index-based lookup first (O(log n)) + if (self.lookupMethodIdentConst(local_type_ident, local_method_ident)) |result| { + return result; + } + + // Fall back to string-based lookup for backward compatibility with pre-compiled modules + // that don't have method_idents populated. This can be removed once all modules are recompiled. + return self.getMethodIdent(type_name, method_name); +} + +/// Looks up a method identifier when the type and method idents come from different source environments. +/// This is needed when e.g. type_ident is from runtime layout store and method_ident is from CIR. +/// Falls back to string-based getMethodIdent for backward compatibility with pre-compiled modules. +pub fn lookupMethodIdentFromTwoEnvsConst( + self: *const Self, + type_source_env: *const Self, + type_ident: Ident.Idx, + method_source_env: *const Self, + method_ident: Ident.Idx, +) ?Ident.Idx { + // Get strings from respective source environments + const type_name = type_source_env.getIdent(type_ident); + const method_name = method_source_env.getIdent(method_ident); + + // Find corresponding idents in this module + const local_type_ident = self.common.findIdent(type_name) orelse return null; + const local_method_ident = self.common.findIdent(method_name) orelse return null; + + // Try index-based lookup first (O(log n)) + if (self.lookupMethodIdentConst(local_type_ident, local_method_ident)) |result| { + return result; + } + + // Fall back to string-based lookup for backward compatibility with pre-compiled modules + // that don't have method_idents populated. This can be removed once all modules are recompiled. + return self.getMethodIdent(type_name, method_name); +} + /// Returns the line start positions for source code position mapping. /// Each element represents the byte offset where a new line begins. pub fn getLineStarts(self: *const Self) []const u32 { diff --git a/src/canonicalize/Monomorphizer.zig b/src/canonicalize/Monomorphizer.zig new file mode 100644 index 0000000000..0f1af11762 --- /dev/null +++ b/src/canonicalize/Monomorphizer.zig @@ -0,0 +1,181 @@ +//! Monomorphizer - Specializes polymorphic functions to concrete types +//! +//! This module implements the monomorphization pass which: +//! 1. Finds all polymorphic function definitions +//! 2. Identifies call sites with concrete types +//! 3. Creates specialized versions of functions for each concrete type +//! +//! Following the Cor approach, monomorphization happens BEFORE lambda set +//! inference, so this module focuses on type-based specialization only. + +const std = @import("std"); +const base = @import("base"); +const types = @import("types"); + +const ModuleEnv = @import("ModuleEnv.zig"); +const CIR = @import("CIR.zig"); +const RocEmitter = @import("RocEmitter.zig"); + +const Self = @This(); + +/// The allocator for intermediate allocations +allocator: std.mem.Allocator, + +/// The module environment containing the CIR +module_env: *ModuleEnv, + +/// The type store for looking up concrete types +types_store: *types.Store, + +/// Map from (original_name, concrete_type_hash) -> specialized_name +specializations: std.AutoHashMap(SpecializationKey, base.Ident.Idx), + +/// Counter for generating unique specialization names +specialization_counter: u32, + +/// Key for looking up specializations +pub const SpecializationKey = struct { + original_ident: base.Ident.Idx, + type_hash: u64, +}; + +/// Initialize the monomorphizer +pub fn init( + allocator: std.mem.Allocator, + module_env: *ModuleEnv, + types_store: *types.Store, +) Self { + return .{ + .allocator = allocator, + .module_env = module_env, + .types_store = types_store, + .specializations = std.AutoHashMap(SpecializationKey, base.Ident.Idx).init(allocator), + .specialization_counter = 0, + }; +} + +/// Free resources +pub fn deinit(self: *Self) void { + self.specializations.deinit(); +} + +/// Check if a type variable represents a polymorphic type +pub fn isPolymorphic(self: *Self, type_var: types.Var) bool { + const resolved = self.types_store.resolveVar(type_var); + return switch (resolved.desc.content) { + .flex, .rigid => true, + .structure, .alias, .recursion_var, .err => false, + }; +} + +/// Get a hash for a concrete type (for use as specialization key) +pub fn typeHash(self: *Self, type_var: types.Var) u64 { + const resolved = self.types_store.resolveVar(type_var); + // Simple hash based on the type's rank and content tag + var hasher = std.hash.Wyhash.init(0); + hasher.update(std.mem.asBytes(&resolved.desc.rank)); + + // Add more detail based on content + switch (resolved.desc.content) { + .structure => |flat_type| { + hasher.update(std.mem.asBytes(&@as(u8, @intFromEnum(flat_type)))); + switch (flat_type) { + .nominal_type => |nom| { + hasher.update(std.mem.asBytes(&nom.ident.ident_idx)); + }, + else => {}, + } + }, + .flex => hasher.update("flex"), + .rigid => hasher.update("rigid"), + .alias => hasher.update("alias"), + .recursion_var => hasher.update("recursion"), + .err => hasher.update("err"), + } + + return hasher.final(); +} + +/// Get the type name for a concrete type (for specialization suffix) +pub fn getTypeName(self: *Self, type_var: types.Var) []const u8 { + const resolved = self.types_store.resolveVar(type_var); + switch (resolved.desc.content) { + .structure => |flat_type| { + switch (flat_type) { + .nominal_type => |nom| { + return self.module_env.getIdent(nom.ident.ident_idx); + }, + else => return "Unknown", + } + }, + .flex, .rigid => return "a", + .alias, .recursion_var, .err => return "Unknown", + } +} + +/// Create a specialized name for a function +pub fn createSpecializedName( + self: *Self, + original_name: base.Ident.Idx, + type_var: types.Var, +) !base.Ident.Idx { + const key = SpecializationKey{ + .original_ident = original_name, + .type_hash = self.typeHash(type_var), + }; + + // Check if we already have this specialization + if (self.specializations.get(key)) |existing| { + return existing; + } + + // Create new specialized name: original_TypeName_N + const original = self.module_env.getIdent(original_name); + const type_name = self.getTypeName(type_var); + self.specialization_counter += 1; + + const specialized = try std.fmt.allocPrint( + self.allocator, + "{s}_{s}_{d}", + .{ original, type_name, self.specialization_counter }, + ); + defer self.allocator.free(specialized); + + const specialized_ident = try self.module_env.insertIdent(base.Ident.for_text(specialized)); + + try self.specializations.put(key, specialized_ident); + return specialized_ident; +} + +// Tests + +const testing = std.testing; + +test "monomorphizer: isPolymorphic with flex var" { + // This test would need a proper types.Store setup + // For now, just verify the type compiles +} + +test "monomorphizer: typeHash produces consistent results" { + // This test would need a proper types.Store setup + // For now, just verify the type compiles +} + +test "monomorphizer: createSpecializedName" { + // Create a minimal test environment + const allocator = testing.allocator; + + const module_env = try allocator.create(ModuleEnv); + module_env.* = try ModuleEnv.init(allocator, "test"); + defer { + module_env.deinit(); + allocator.destroy(module_env); + } + + // Note: We'd need a proper types.Store to fully test this + // For now, just verify creation and cleanup work + var mono = Self.init(allocator, module_env, undefined); + defer mono.deinit(); + + try testing.expectEqual(@as(u32, 0), mono.specialization_counter); +} diff --git a/src/canonicalize/Node.zig b/src/canonicalize/Node.zig index efedb6a2de..e8e90f5182 100644 --- a/src/canonicalize/Node.zig +++ b/src/canonicalize/Node.zig @@ -25,6 +25,7 @@ pub const Idx = List.Idx; pub const Tag = enum { // Statements statement_decl, + statement_decl_gen, statement_var, statement_reassign, statement_crash, @@ -32,11 +33,13 @@ pub const Tag = enum { statement_expr, statement_expect, statement_for, + statement_while, statement_return, statement_import, statement_alias_decl, statement_nominal_decl, statement_type_anno, + statement_type_var_alias, // Expressions expr_var, expr_tuple, @@ -50,6 +53,7 @@ pub const Tag = enum { expr_field_access, expr_static_dispatch, expr_external_lookup, + expr_required_lookup, expr_dot_access, expr_apply, expr_string, @@ -58,7 +62,7 @@ pub const Tag = enum { expr_int, expr_frac_f32, expr_frac_f64, - expr_frac_dec, + expr_dec, expr_dec_small, expr_tag, expr_nominal, @@ -77,20 +81,27 @@ pub const Tag = enum { expr_crash, expr_block, expr_ellipsis, + expr_anno_only, + expr_hosted_lambda, + expr_low_level, expr_expect, + expr_for, expr_record_builder, + expr_return, + expr_type_var_dispatch, match_branch, match_branch_pattern, - where_clause, type_header, annotation, // Type Annotation ty_apply, ty_apply_external, - ty_var, - ty_ident, + ty_rigid_var, + ty_rigid_var_lookup, + ty_lookup, ty_underscore, ty_tag_union, + ty_tag, ty_tuple, ty_record, ty_record_field, @@ -98,6 +109,10 @@ pub const Tag = enum { ty_parens, ty_lookup_external, ty_malformed, + // Where clause + where_method, + where_alias, + where_malformed, // Patterns pattern_identifier, pattern_as, @@ -108,7 +123,6 @@ pub const Tag = enum { pattern_list, pattern_tuple, pattern_num_literal, - pattern_int_literal, pattern_dec_literal, pattern_f32_literal, pattern_f64_literal, @@ -145,11 +159,11 @@ pub const Tag = enum { // diagnostic indices stored in malformed nodes. diag_not_implemented, diag_invalid_num_literal, - diag_invalid_single_quote, diag_empty_single_quote, diag_empty_tuple, diag_ident_already_in_scope, diag_ident_not_in_scope, + diag_qualified_ident_does_not_exist, diag_invalid_top_level_statement, diag_expr_not_canonicalized, diag_invalid_string_interpolation, @@ -163,6 +177,15 @@ pub const Tag = enum { diag_malformed_type_annotation, diag_malformed_where_clause, diag_where_clause_not_allowed_in_type_decl, + diag_type_module_missing_matching_type, + diag_default_app_missing_main, + diag_default_app_wrong_arity, + diag_cannot_import_default_app, + diag_execution_requires_app_or_default_app, + diag_type_name_case_mismatch, + diag_module_header_deprecated, + diag_redundant_expose_main_type, + diag_invalid_main_type_rename_in_exposing, diag_var_across_function_boundary, diag_shadowing_warning, diag_type_redeclared, @@ -174,7 +197,10 @@ pub const Tag = enum { diag_module_not_found, diag_value_not_exposed, diag_type_not_exposed, + diag_type_from_missing_module, diag_module_not_imported, + diag_nested_type_not_found, + diag_nested_value_not_found, diag_too_many_exports, diag_nominal_type_redeclared, diag_type_shadowed_warning, @@ -186,8 +212,9 @@ pub const Tag = enum { diag_f64_pattern_literal, diag_unused_type_var_name, diag_type_var_marked_unused, - diag_type_var_ending_in_underscore, + diag_type_var_starting_with_dollar, diag_underscore_in_type_declaration, diagnostic_exposed_but_not_implemented, diag_redundant_exposed, + diag_if_expr_without_else, }; diff --git a/src/canonicalize/NodeStore.zig b/src/canonicalize/NodeStore.zig index 72e79a957e..3353f96dc1 100644 --- a/src/canonicalize/NodeStore.zig +++ b/src/canonicalize/NodeStore.zig @@ -13,6 +13,7 @@ const CIR = @import("CIR.zig"); const SERIALIZATION_ALIGNMENT = collections.SERIALIZATION_ALIGNMENT; +const Allocator = std.mem.Allocator; const CompactWriter = collections.CompactWriter; const SafeList = collections.SafeList; const RocDec = builtins.dec.RocDec; @@ -23,59 +24,95 @@ const Ident = base.Ident; const PackedDataSpan = base.PackedDataSpan; const FunctionArgs = base.FunctionArgs; +/// When storing optional indices/values where 0 is a valid value, we add this offset +/// to distinguish "value is 0" from "value is null". This is a common pattern when +/// packing optional data into u32 fields where 0 would otherwise be ambiguous. +const OPTIONAL_VALUE_OFFSET: u32 = 1; + const NodeStore = @This(); -gpa: std.mem.Allocator, +gpa: Allocator, nodes: Node.List, regions: Region.List, extra_data: collections.SafeList(u32), -scratch_statements: base.Scratch(CIR.Statement.Idx), -scratch_exprs: base.Scratch(CIR.Expr.Idx), -scratch_record_fields: base.Scratch(CIR.RecordField.Idx), -scratch_match_branches: base.Scratch(CIR.Expr.Match.Branch.Idx), -scratch_match_branch_patterns: base.Scratch(CIR.Expr.Match.BranchPattern.Idx), -scratch_if_branches: base.Scratch(CIR.Expr.IfBranch.Idx), -scratch_where_clauses: base.Scratch(CIR.WhereClause.Idx), -scratch_patterns: base.Scratch(CIR.Pattern.Idx), -scratch_pattern_record_fields: base.Scratch(CIR.PatternRecordField.Idx), -scratch_record_destructs: base.Scratch(CIR.Pattern.RecordDestruct.Idx), -scratch_type_annos: base.Scratch(CIR.TypeAnno.Idx), -scratch_anno_record_fields: base.Scratch(CIR.TypeAnno.RecordField.Idx), -scratch_exposed_items: base.Scratch(CIR.ExposedItem.Idx), -scratch_defs: base.Scratch(CIR.Def.Idx), -scratch_diagnostics: base.Scratch(CIR.Diagnostic.Idx), -scratch_captures: base.Scratch(CIR.Expr.Capture.Idx), +scratch: ?*Scratch, // Nullable because when we deserialize a NodeStore, we don't bother to reinitialize scratch. + +const Scratch = struct { + statements: base.Scratch(CIR.Statement.Idx), + exprs: base.Scratch(CIR.Expr.Idx), + record_fields: base.Scratch(CIR.RecordField.Idx), + match_branches: base.Scratch(CIR.Expr.Match.Branch.Idx), + match_branch_patterns: base.Scratch(CIR.Expr.Match.BranchPattern.Idx), + if_branches: base.Scratch(CIR.Expr.IfBranch.Idx), + where_clauses: base.Scratch(CIR.WhereClause.Idx), + patterns: base.Scratch(CIR.Pattern.Idx), + record_destructs: base.Scratch(CIR.Pattern.RecordDestruct.Idx), + type_annos: base.Scratch(CIR.TypeAnno.Idx), + anno_record_fields: base.Scratch(CIR.TypeAnno.RecordField.Idx), + exposed_items: base.Scratch(CIR.ExposedItem.Idx), + defs: base.Scratch(CIR.Def.Idx), + diagnostics: base.Scratch(CIR.Diagnostic.Idx), + captures: base.Scratch(CIR.Expr.Capture.Idx), + + fn init(gpa: Allocator) Allocator.Error!*@This() { + const ptr = try gpa.create(Scratch); + + ptr.* = .{ + .statements = try base.Scratch(CIR.Statement.Idx).init(gpa), + .exprs = try base.Scratch(CIR.Expr.Idx).init(gpa), + .record_fields = try base.Scratch(CIR.RecordField.Idx).init(gpa), + .match_branches = try base.Scratch(CIR.Expr.Match.Branch.Idx).init(gpa), + .match_branch_patterns = try base.Scratch(CIR.Expr.Match.BranchPattern.Idx).init(gpa), + .if_branches = try base.Scratch(CIR.Expr.IfBranch.Idx).init(gpa), + .where_clauses = try base.Scratch(CIR.WhereClause.Idx).init(gpa), + .patterns = try base.Scratch(CIR.Pattern.Idx).init(gpa), + .record_destructs = try base.Scratch(CIR.Pattern.RecordDestruct.Idx).init(gpa), + .type_annos = try base.Scratch(CIR.TypeAnno.Idx).init(gpa), + .anno_record_fields = try base.Scratch(CIR.TypeAnno.RecordField.Idx).init(gpa), + .exposed_items = try base.Scratch(CIR.ExposedItem.Idx).init(gpa), + .defs = try base.Scratch(CIR.Def.Idx).init(gpa), + .diagnostics = try base.Scratch(CIR.Diagnostic.Idx).init(gpa), + .captures = try base.Scratch(CIR.Expr.Capture.Idx).init(gpa), + }; + + return ptr; + } + + fn deinit(self: *@This(), gpa: Allocator) void { + self.statements.deinit(); + self.exprs.deinit(); + self.record_fields.deinit(); + self.match_branches.deinit(); + self.match_branch_patterns.deinit(); + self.if_branches.deinit(); + self.where_clauses.deinit(); + self.patterns.deinit(); + self.record_destructs.deinit(); + self.type_annos.deinit(); + self.anno_record_fields.deinit(); + self.exposed_items.deinit(); + self.defs.deinit(); + self.diagnostics.deinit(); + self.captures.deinit(); + gpa.destroy(self); + } +}; /// Initializes the NodeStore -pub fn init(gpa: std.mem.Allocator) std.mem.Allocator.Error!NodeStore { +pub fn init(gpa: Allocator) Allocator.Error!NodeStore { // TODO determine what capacity to use // maybe these should be moved to build/compile flags? return try NodeStore.initCapacity(gpa, 128); } /// Initializes the NodeStore with a specified capacity. -pub fn initCapacity(gpa: std.mem.Allocator, capacity: usize) std.mem.Allocator.Error!NodeStore { +pub fn initCapacity(gpa: Allocator, capacity: usize) Allocator.Error!NodeStore { return .{ .gpa = gpa, .nodes = try Node.List.initCapacity(gpa, capacity), .regions = try Region.List.initCapacity(gpa, capacity), .extra_data = try collections.SafeList(u32).initCapacity(gpa, capacity / 2), - .scratch_statements = try base.Scratch(CIR.Statement.Idx).init(gpa), - .scratch_exprs = try base.Scratch(CIR.Expr.Idx).init(gpa), - .scratch_record_fields = try base.Scratch(CIR.RecordField.Idx).init(gpa), - .scratch_match_branches = try base.Scratch(CIR.Expr.Match.Branch.Idx).init(gpa), - .scratch_match_branch_patterns = try base.Scratch(CIR.Expr.Match.BranchPattern.Idx).init(gpa), - .scratch_if_branches = try base.Scratch(CIR.Expr.IfBranch.Idx).init(gpa), - .scratch_where_clauses = try base.Scratch(CIR.WhereClause.Idx).init(gpa), - .scratch_patterns = try base.Scratch(CIR.Pattern.Idx).init(gpa), - .scratch_pattern_record_fields = try base.Scratch(CIR.PatternRecordField.Idx).init(gpa), - .scratch_record_destructs = try base.Scratch(CIR.Pattern.RecordDestruct.Idx).init(gpa), - .scratch_type_annos = try base.Scratch(CIR.TypeAnno.Idx).init(gpa), - .scratch_anno_record_fields = try base.Scratch(CIR.TypeAnno.RecordField.Idx).init(gpa), - .scratch_exposed_items = try base.Scratch(CIR.ExposedItem.Idx).init(gpa), - .scratch_defs = try base.Scratch(CIR.Def.Idx).init(gpa), - .scratch_diagnostics = try base.Scratch(CIR.Diagnostic.Idx).init(gpa), - .scratch_captures = try base.Scratch(CIR.Expr.Capture.Idx).init(gpa), + .scratch = try Scratch.init(gpa), }; } @@ -84,33 +121,29 @@ pub fn deinit(store: *NodeStore) void { store.nodes.deinit(store.gpa); store.regions.deinit(store.gpa); store.extra_data.deinit(store.gpa); - store.scratch_statements.deinit(store.gpa); - store.scratch_exprs.deinit(store.gpa); - store.scratch_record_fields.deinit(store.gpa); - store.scratch_match_branches.deinit(store.gpa); - store.scratch_match_branch_patterns.deinit(store.gpa); - store.scratch_if_branches.deinit(store.gpa); - store.scratch_where_clauses.deinit(store.gpa); - store.scratch_patterns.deinit(store.gpa); - store.scratch_pattern_record_fields.deinit(store.gpa); - store.scratch_record_destructs.deinit(store.gpa); - store.scratch_type_annos.deinit(store.gpa); - store.scratch_anno_record_fields.deinit(store.gpa); - store.scratch_exposed_items.deinit(store.gpa); - store.scratch_defs.deinit(store.gpa); - store.scratch_diagnostics.deinit(store.gpa); - store.scratch_captures.deinit(store.gpa); + if (store.scratch) |scratch| { + scratch.deinit(store.gpa); + } +} + +/// Add the given offset to the memory addresses of all pointers in `self`. +/// This is used when loading a NodeStore from shared memory at a different address. +pub fn relocate(store: *NodeStore, offset: isize) void { + store.nodes.relocate(offset); + store.regions.relocate(offset); + store.extra_data.relocate(offset); + // scratch is null for deserialized NodeStores, no need to relocate } /// Compile-time constants for union variant counts to ensure we don't miss cases /// when adding/removing variants from ModuleEnv unions. Update these when modifying the unions. /// /// Count of the diagnostic nodes in the ModuleEnv -pub const MODULEENV_DIAGNOSTIC_NODE_COUNT = 46; +pub const MODULEENV_DIAGNOSTIC_NODE_COUNT = 59; /// Count of the expression nodes in the ModuleEnv -pub const MODULEENV_EXPR_NODE_COUNT = 33; +pub const MODULEENV_EXPR_NODE_COUNT = 40; /// Count of the statement nodes in the ModuleEnv -pub const MODULEENV_STATEMENT_NODE_COUNT = 14; +pub const MODULEENV_STATEMENT_NODE_COUNT = 17; /// Count of the type annotation nodes in the ModuleEnv pub const MODULEENV_TYPE_ANNO_NODE_COUNT = 12; /// Count of the pattern nodes in the ModuleEnv @@ -176,6 +209,12 @@ pub fn getTypeAnnoRegion(store: *const NodeStore, type_anno_idx: CIR.TypeAnno.Id return store.getRegionAt(node_idx); } +/// Helper function to get a region by annotation index +pub fn getAnnotationRegion(store: *const NodeStore, anno_idx: CIR.Annotation.Idx) Region { + const node_idx: Node.Idx = @enumFromInt(@intFromEnum(anno_idx)); + return store.getRegionAt(node_idx); +} + /// Retrieves a region from node from the store. pub fn getNodeRegion(store: *const NodeStore, node_idx: Node.Idx) Region { return store.getRegionAt(node_idx); @@ -203,6 +242,22 @@ pub fn getStatement(store: *const NodeStore, statement: CIR.Statement.Idx) CIR.S }, } }; }, + .statement_decl_gen => { + return CIR.Statement{ .s_decl_gen = .{ + .pattern = @enumFromInt(node.data_1), + .expr = @enumFromInt(node.data_2), + .anno = blk: { + const extra_start = node.data_3; + const extra_data = store.extra_data.items.items[extra_start..]; + const has_anno = extra_data[0] != 0; + if (has_anno) { + break :blk @as(CIR.Annotation.Idx, @enumFromInt(extra_data[1])); + } else { + break :blk null; + } + }, + } }; + }, .statement_var => return CIR.Statement{ .s_var = .{ .pattern_idx = @enumFromInt(node.data_1), .expr = @enumFromInt(node.data_2), @@ -228,8 +283,13 @@ pub fn getStatement(store: *const NodeStore, statement: CIR.Statement.Idx) CIR.S .expr = @enumFromInt(node.data_2), .body = @enumFromInt(node.data_3), } }, + .statement_while => return CIR.Statement{ .s_while = .{ + .cond = @enumFromInt(node.data_1), + .body = @enumFromInt(node.data_2), + } }, .statement_return => return CIR.Statement{ .s_return = .{ .expr = @enumFromInt(node.data_1), + .lambda = if (node.data_2 == 0) null else @as(?CIR.Expr.Idx, @enumFromInt(node.data_2 - 1)), } }, .statement_import => { const extra_start = node.data_2; @@ -262,10 +322,14 @@ pub fn getStatement(store: *const NodeStore, statement: CIR.Statement.Idx) CIR.S }; }, .statement_nominal_decl => { + // Get is_opaque from extra_data + const extra_idx = node.data_3; + const is_opaque = store.extra_data.items.items[extra_idx] != 0; return CIR.Statement{ .s_nominal_decl = .{ .header = @as(CIR.TypeHeader.Idx, @enumFromInt(node.data_1)), .anno = @as(CIR.TypeAnno.Idx, @enumFromInt(node.data_2)), + .is_opaque = is_opaque, }, }; }, @@ -291,6 +355,15 @@ pub fn getStatement(store: *const NodeStore, statement: CIR.Statement.Idx) CIR.S }, }; }, + .statement_type_var_alias => { + return CIR.Statement{ + .s_type_var_alias = .{ + .alias_name = @bitCast(node.data_1), + .type_var_name = @bitCast(node.data_2), + .type_var_anno = @enumFromInt(node.data_3), + }, + }; + }, .malformed => { return CIR.Statement{ .s_runtime_error = .{ .diagnostic = @enumFromInt(node.data_1), @@ -323,21 +396,31 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { .region = store.getRegionAt(node_idx), } }; }, - .expr_int => { + .expr_required_lookup => { + // Handle required lookups (platform requires clause) + return CIR.Expr{ .e_lookup_required = .{ + .requires_idx = ModuleEnv.RequiredType.SafeList.Idx.fromU32(node.data_1), + } }; + }, + .expr_num => { + // Get requirements + const kind: CIR.NumKind = @enumFromInt(node.data_1); + // Read i128 from extra_data (stored as 4 u32s in data_1) - const value_as_u32s = store.extra_data.items.items[node.data_1..][0..4]; + const val_kind: CIR.IntValue.IntKind = @enumFromInt(node.data_2); + const value_as_u32s = store.extra_data.items.items[node.data_3..][0..4]; // Retrieve type variable from data_2 and requirements from data_3 return CIR.Expr{ - .e_int = .{ - .value = .{ .bytes = @bitCast(value_as_u32s.*), .kind = .i128 }, + .e_num = .{ + .value = .{ .bytes = @bitCast(value_as_u32s.*), .kind = val_kind }, + .kind = kind, }, }; }, .expr_list => { return CIR.Expr{ .e_list = .{ - .elem_var = @enumFromInt(node.data_3), .elems = .{ .span = .{ .start = node.data_1, .len = node.data_2 } }, }, }; @@ -351,7 +434,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { }, .expr_call => { // Retrieve args span from extra_data - const extra_start = node.data_1; + const extra_start = node.data_2; const extra_data = store.extra_data.items.items[extra_start..]; const args_start = extra_data[0]; @@ -359,26 +442,34 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { return CIR.Expr{ .e_call = .{ + .func = @enumFromInt(node.data_1), .args = .{ .span = .{ .start = args_start, .len = args_len } }, - .called_via = @enumFromInt(node.data_2), + .called_via = @enumFromInt(node.data_3), }, }; }, - .expr_frac_f32 => return CIR.Expr{ .e_frac_f32 = .{ .value = @bitCast(node.data_1) } }, + .expr_frac_f32 => return CIR.Expr{ .e_frac_f32 = .{ + .value = @bitCast(node.data_1), + .has_suffix = node.data_2 != 0, + } }, .expr_frac_f64 => { const raw: [2]u32 = .{ node.data_1, node.data_2 }; - return CIR.Expr{ .e_frac_f64 = .{ .value = @bitCast(raw) } }; + return CIR.Expr{ .e_frac_f64 = .{ + .value = @bitCast(raw), + .has_suffix = node.data_3 != 0, + } }; }, - .expr_frac_dec => { + .expr_dec => { // Get value from extra_data const extra_data_idx = node.data_1; const value_as_u32s = store.extra_data.items.items[extra_data_idx..][0..4]; const value_as_i128: i128 = @bitCast(value_as_u32s.*); return CIR.Expr{ - .e_frac_dec = .{ + .e_dec = .{ .value = RocDec{ .num = value_as_i128 }, + .has_suffix = node.data_2 != 0, }, }; }, @@ -387,12 +478,15 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { // data_1: numerator (i16) stored as u32 // data_3: denominator_power_of_ten (u8) in lower 8 bits const numerator = @as(i16, @intCast(@as(i32, @bitCast(node.data_1)))); - const denominator_power_of_ten = @as(u8, @truncate(node.data_3)); + const denominator_power_of_ten = @as(u8, @truncate(node.data_2)); return CIR.Expr{ .e_dec_small = .{ - .numerator = numerator, - .denominator_power_of_ten = denominator_power_of_ten, + .value = .{ + .numerator = numerator, + .denominator_power_of_ten = denominator_power_of_ten, + }, + .has_suffix = node.data_3 != 0, }, }; }, @@ -578,18 +672,85 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { .expr_suffix_single_question, .expr_record_builder, => { - return CIR.Expr{ .e_runtime_error = .{ - .diagnostic = @enumFromInt(0), - } }; + return CIR.Expr{ + .e_runtime_error = .{ + .diagnostic = undefined, // deserialized runtime errors don't preserve diagnostics + }, + }; }, .expr_ellipsis => { return CIR.Expr{ .e_ellipsis = .{} }; }, + .expr_anno_only => { + return CIR.Expr{ .e_anno_only = .{} }; + }, + .expr_return => { + return CIR.Expr{ .e_return = .{ + .expr = @enumFromInt(node.data_1), + } }; + }, + .expr_type_var_dispatch => { + // Retrieve type var dispatch data from node and extra_data + const type_var_alias_stmt: CIR.Statement.Idx = @enumFromInt(node.data_1); + const method_name: base.Ident.Idx = @bitCast(node.data_2); + const extra_start = node.data_3; + const extra_data = store.extra_data.items.items[extra_start..]; + + const args_start = extra_data[0]; + const args_len = extra_data[1]; + + return CIR.Expr{ .e_type_var_dispatch = .{ + .type_var_alias_stmt = type_var_alias_stmt, + .method_name = method_name, + .args = .{ .span = .{ .start = args_start, .len = args_len } }, + } }; + }, + .expr_hosted_lambda => { + // Retrieve hosted lambda data from node and extra_data + const symbol_name: base.Ident.Idx = @bitCast(node.data_1); + const index = node.data_2; + const extra_start = node.data_3; + const extra_data = store.extra_data.items.items[extra_start..]; + + const args_start = extra_data[0]; + const args_len = extra_data[1]; + const body_idx = extra_data[2]; + + return CIR.Expr{ .e_hosted_lambda = .{ + .symbol_name = symbol_name, + .index = index, + .args = .{ .span = .{ .start = args_start, .len = args_len } }, + .body = @enumFromInt(body_idx), + } }; + }, + .expr_low_level => { + // Retrieve low-level lambda data from extra_data + const op: CIR.Expr.LowLevel = @enumFromInt(node.data_1); + const extra_start = node.data_2; + const extra_data = store.extra_data.items.items[extra_start..]; + + const args_start = extra_data[0]; + const args_len = extra_data[1]; + const body_idx = extra_data[2]; + + return CIR.Expr{ .e_low_level_lambda = .{ + .op = op, + .args = .{ .span = .{ .start = args_start, .len = args_len } }, + .body = @enumFromInt(body_idx), + } }; + }, .expr_expect => { return CIR.Expr{ .e_expect = .{ .body = @enumFromInt(node.data_1), } }; }, + .expr_for => { + return CIR.Expr{ .e_for = .{ + .patt = @enumFromInt(node.data_1), + .expr = @enumFromInt(node.data_2), + .body = @enumFromInt(node.data_3), + } }; + }, .expr_if_then_else => { const extra_start = node.data_1; const extra_data = store.extra_data.items.items[extra_start..]; @@ -610,8 +771,16 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { } }; }, .expr_dot_access => { - const args_span = if (node.data_3 != 0) blk: { - const packed_span = FunctionArgs.fromU32(node.data_3); + // Read extra data: field_name_region (2 u32s) + optional args (1 u32) + const extra_start = node.data_3; + const extra_data = store.extra_data.items.items[extra_start..]; + const field_name_region = base.Region{ + .start = .{ .offset = extra_data[0] }, + .end = .{ .offset = extra_data[1] }, + }; + const args_packed = extra_data[2]; + const args_span = if (args_packed != 0) blk: { + const packed_span = FunctionArgs.fromU32(args_packed - OPTIONAL_VALUE_OFFSET); const data_span = packed_span.toDataSpan(); break :blk CIR.Expr.Span{ .span = data_span }; } else null; @@ -619,6 +788,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { return CIR.Expr{ .e_dot_access = .{ .receiver = @enumFromInt(node.data_1), .field_name = @bitCast(node.data_2), + .field_name_region = field_name_region, .args = args_span, } }; }, @@ -638,6 +808,54 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { } } +/// Replaces an existing expression with an e_num expression in-place. +/// This is used for constant folding during compile-time evaluation. +/// Note: This modifies only the CIR node and should only be called after type-checking +/// is complete. Type information is stored separately and remains unchanged. +pub fn replaceExprWithNum(store: *NodeStore, expr_idx: CIR.Expr.Idx, value: CIR.IntValue, num_kind: CIR.NumKind) !void { + const node_idx: Node.Idx = @enumFromInt(@intFromEnum(expr_idx)); + + const extra_data_start = store.extra_data.len(); + const value_as_i128: i128 = @bitCast(value.bytes); + const value_as_u32s: [4]u32 = @bitCast(value_as_i128); + _ = try store.extra_data.appendSlice(store.gpa, &value_as_u32s); + + store.nodes.set(node_idx, .{ + .tag = .expr_num, + .data_1 = @intFromEnum(num_kind), + .data_2 = @intFromEnum(value.kind), + .data_3 = @intCast(extra_data_start), + }); +} + +/// Replaces an existing expression with an e_zero_argument_tag expression in-place. +/// This is used for constant folding tag unions (like Bool) during compile-time evaluation. +/// Note: This modifies only the CIR node and should only be called after type-checking +/// is complete. Type information is stored separately and remains unchanged. +pub fn replaceExprWithZeroArgumentTag( + store: *NodeStore, + expr_idx: CIR.Expr.Idx, + closure_name: Ident.Idx, + variant_var: types.Var, + ext_var: types.Var, + name: Ident.Idx, +) !void { + const node_idx: Node.Idx = @enumFromInt(@intFromEnum(expr_idx)); + + const extra_data_start = store.extra_data.len(); + _ = try store.extra_data.append(store.gpa, @bitCast(closure_name)); + _ = try store.extra_data.append(store.gpa, @intFromEnum(variant_var)); + _ = try store.extra_data.append(store.gpa, @intFromEnum(ext_var)); + _ = try store.extra_data.append(store.gpa, @bitCast(name)); + + store.nodes.set(node_idx, .{ + .tag = .expr_zero_argument_tag, + .data_1 = @intCast(extra_data_start), + .data_2 = 0, + .data_3 = 0, + }); +} + /// Get the more-specific expr index. Used to make error messages nicer. /// /// For example, if the provided expr is a `block`, then this will return the @@ -703,56 +921,44 @@ pub fn getWhereClause(store: *const NodeStore, whereClause: CIR.WhereClause.Idx) const node_idx: Node.Idx = @enumFromInt(@intFromEnum(whereClause)); const node = store.nodes.get(node_idx); - std.debug.assert(node.tag == .where_clause); + switch (node.tag) { + .where_method => { + const var_ = @as(CIR.TypeAnno.Idx, @enumFromInt(node.data_1)); + const method_name = @as(Ident.Idx, @bitCast(node.data_2)); - // Retrieve where clause data from extra_data - const extra_start = node.data_1; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_start = node.data_3; + const extra_data = store.extra_data.items.items[extra_start..]; - const discriminant = extra_data[0]; + const args_start = extra_data[0]; + const args_len = extra_data[1]; + const ret = @as(CIR.TypeAnno.Idx, @enumFromInt(extra_data[2])); - switch (discriminant) { - 0 => { // mod_method - const var_name = @as(Ident.Idx, @bitCast(extra_data[1])); - const method_name = @as(Ident.Idx, @bitCast(extra_data[2])); - const args_start = extra_data[3]; - const args_len = extra_data[4]; - const ret_anno = @as(CIR.TypeAnno.Idx, @enumFromInt(extra_data[5])); - const external_decl = @as(CIR.ExternalDecl.Idx, @enumFromInt(extra_data[6])); - - return CIR.WhereClause{ - .mod_method = .{ - .var_name = var_name, - .method_name = method_name, - .args = .{ .span = .{ .start = args_start, .len = args_len } }, - .ret_anno = ret_anno, - .external_decl = external_decl, - }, - }; + return CIR.WhereClause{ .w_method = .{ + .var_ = var_, + .method_name = method_name, + .args = .{ .span = .{ .start = args_start, .len = args_len } }, + .ret = ret, + } }; }, - 1 => { // mod_alias - const var_name = @as(Ident.Idx, @bitCast(extra_data[1])); - const alias_name = @as(Ident.Idx, @bitCast(extra_data[2])); - const external_decl = @as(CIR.ExternalDecl.Idx, @enumFromInt(extra_data[3])); + .where_alias => { + const var_ = @as(CIR.TypeAnno.Idx, @enumFromInt(node.data_1)); + const alias_name = @as(Ident.Idx, @bitCast(node.data_2)); - return CIR.WhereClause{ - .mod_alias = .{ - .var_name = var_name, - .alias_name = alias_name, - .external_decl = external_decl, - }, - }; + return CIR.WhereClause{ .w_alias = .{ + .var_ = var_, + .alias_name = alias_name, + } }; }, - 2 => { // malformed - const diagnostic = @as(CIR.Diagnostic.Idx, @enumFromInt(extra_data[1])); + .where_malformed => { + const diagnostic = @as(CIR.Diagnostic.Idx, @enumFromInt(node.data_1)); - return CIR.WhereClause{ - .malformed = .{ - .diagnostic = diagnostic, - }, - }; + return CIR.WhereClause{ .w_malformed = .{ + .diagnostic = diagnostic, + } }; + }, + else => { + std.debug.panic("unreachable, node is not a where tag {}", .{node.tag}); }, - else => @panic("Invalid where clause discriminant"), } } @@ -815,19 +1021,12 @@ pub fn getPattern(store: *const NodeStore, pattern_idx: CIR.Pattern.Idx) CIR.Pat }; }, .pattern_record_destructure => { - const extra_start = node.data_1; - const extra_data = store.extra_data.items.items[extra_start..]; - - const destructs_start = extra_data[0]; - const destructs_len = extra_data[1]; - const ext_var = @as(types.Var, @enumFromInt(extra_data[2])); - const whole_var = @as(types.Var, @enumFromInt(extra_data[3])); + const destructs_start = node.data_1; + const destructs_len = node.data_2; return CIR.Pattern{ .record_destructure = .{ .destructs = DataSpan.init(destructs_start, destructs_len).as(CIR.Pattern.RecordDestruct.Span), - .ext_var = ext_var, - .whole_var = whole_var, }, }; }, @@ -837,16 +1036,14 @@ pub fn getPattern(store: *const NodeStore, pattern_idx: CIR.Pattern.Idx) CIR.Pat const patterns_start = extra_data[0]; const patterns_len = extra_data[1]; - const elem_var = @as(types.Var, @enumFromInt(extra_data[2])); - const list_var = @as(types.Var, @enumFromInt(extra_data[3])); // Load rest_info - const has_rest_info = extra_data[4] != 0; + const has_rest_info = extra_data[2] != 0; const rest_info = if (has_rest_info) blk: { - const rest_index = extra_data[5]; - const has_pattern = extra_data[6] != 0; + const rest_index = extra_data[3]; + const has_pattern = extra_data[4] != 0; const rest_pattern = if (has_pattern) - @as(CIR.Pattern.Idx, @enumFromInt(extra_data[7])) + @as(CIR.Pattern.Idx, @enumFromInt(extra_data[5])) else null; break :blk @as(@TypeOf(@as(CIR.Pattern, undefined).list.rest_info), .{ @@ -858,8 +1055,6 @@ pub fn getPattern(store: *const NodeStore, pattern_idx: CIR.Pattern.Idx) CIR.Pat return CIR.Pattern{ .list = .{ .patterns = DataSpan.init(patterns_start, patterns_len).as(CIR.Pattern.Span), - .elem_var = elem_var, - .list_var = list_var, .rest_info = rest_info, }, }; @@ -870,24 +1065,18 @@ pub fn getPattern(store: *const NodeStore, pattern_idx: CIR.Pattern.Idx) CIR.Pat }, }, .pattern_num_literal => { - const extra_data_idx = node.data_1; + const kind: CIR.NumKind = @enumFromInt(node.data_1); + + const val_kind: CIR.IntValue.IntKind = @enumFromInt(node.data_2); + + const extra_data_idx = node.data_3; const value_as_u32s = store.extra_data.items.items[extra_data_idx..][0..4]; const value_as_i128: i128 = @bitCast(value_as_u32s.*); return CIR.Pattern{ - .int_literal = .{ - .value = .{ .bytes = @bitCast(value_as_i128), .kind = .i128 }, - }, - }; - }, - .pattern_int_literal => { - const extra_data_idx = node.data_1; - const value_as_u32s = store.extra_data.items.items[extra_data_idx..][0..4]; - const value_as_i128: i128 = @bitCast(value_as_u32s.*); - - return CIR.Pattern{ - .int_literal = .{ - .value = .{ .bytes = @bitCast(value_as_i128), .kind = .i128 }, + .num_literal = .{ + .value = .{ .bytes = @bitCast(value_as_i128), .kind = val_kind }, + .kind = kind, }, }; }, @@ -908,9 +1097,12 @@ pub fn getPattern(store: *const NodeStore, pattern_idx: CIR.Pattern.Idx) CIR.Pat const value_as_u32s = store.extra_data.items.items[extra_data_idx..][0..4]; const value_as_i128: i128 = @bitCast(value_as_u32s.*); + const has_suffix = node.data_2 != 0; + return CIR.Pattern{ .dec_literal = .{ .value = RocDec{ .num = value_as_i128 }, + .has_suffix = has_suffix, }, }; }, @@ -919,12 +1111,17 @@ pub fn getPattern(store: *const NodeStore, pattern_idx: CIR.Pattern.Idx) CIR.Pat // data_1: numerator (i16) stored as u32 // data_3: denominator_power_of_ten (u8) in lower 8 bits const numerator: i16 = @intCast(@as(i32, @bitCast(node.data_1))); - const denominator_power_of_ten: u8 = @intCast(node.data_3 & 0xFF); + const denominator_power_of_ten: u8 = @intCast(node.data_2 & 0xFF); + + const has_suffix = node.data_3 != 0; return CIR.Pattern{ .small_dec_literal = .{ - .numerator = numerator, - .denominator_power_of_ten = denominator_power_of_ten, + .value = .{ + .numerator = numerator, + .denominator_power_of_ten = denominator_power_of_ten, + }, + .has_suffix = has_suffix, }, }; }, @@ -939,56 +1136,98 @@ pub fn getPattern(store: *const NodeStore, pattern_idx: CIR.Pattern.Idx) CIR.Pat } }; }, else => { - @panic("unreachable, node is not an pattern tag"); + std.debug.panic("unreachable, node is not a pattern tag {}", .{node.tag}); }, } } -/// Retrieves a pattern record field from the store. -pub fn getPatternRecordField(store: *NodeStore, patternRecordField: CIR.PatternRecordField.Idx) CIR.PatternRecordField { - _ = store; - _ = patternRecordField; - // Return empty placeholder since PatternRecordField has no fields yet - return CIR.PatternRecordField{}; -} - /// Retrieves a type annotation from the store. pub fn getTypeAnno(store: *const NodeStore, typeAnno: CIR.TypeAnno.Idx) CIR.TypeAnno { const node_idx: Node.Idx = @enumFromInt(@intFromEnum(typeAnno)); const node = store.nodes.get(node_idx); switch (node.tag) { - .ty_apply => return CIR.TypeAnno{ .apply = .{ - .symbol = @bitCast(node.data_1), - .args = .{ .span = .{ .start = node.data_2, .len = node.data_3 } }, - } }, - .ty_apply_external => { - const extra_data_idx = node.data_3; - const args_start = store.extra_data.items.items[extra_data_idx]; - const args_len = store.extra_data.items.items[extra_data_idx + 1]; - return CIR.TypeAnno{ .apply_external = .{ - .module_idx = @enumFromInt(node.data_1), - .target_node_idx = @intCast(node.data_2), + .ty_apply => { + const name: Ident.Idx = @bitCast(node.data_1); + const args_start = node.data_2; + + const extra_data = store.extra_data.items.items[node.data_3..]; + const args_len = extra_data[0]; + const base_enum: CIR.TypeAnno.LocalOrExternal.Tag = @enumFromInt(extra_data[1]); + const type_base: CIR.TypeAnno.LocalOrExternal = blk: { + switch (base_enum) { + .builtin => { + break :blk .{ .builtin = @enumFromInt(extra_data[2]) }; + }, + .local => { + break :blk .{ .local = .{ .decl_idx = @enumFromInt(extra_data[2]) } }; + }, + .external => { + break :blk .{ .external = .{ + .module_idx = @enumFromInt(extra_data[2]), + .target_node_idx = @intCast(extra_data[3]), + } }; + }, + } + }; + + return CIR.TypeAnno{ .apply = .{ + .name = name, + .base = type_base, .args = .{ .span = .{ .start = args_start, .len = args_len } }, } }; }, - .ty_var => return CIR.TypeAnno{ .ty_var = .{ + .ty_rigid_var => return CIR.TypeAnno{ .rigid_var = .{ .name = @bitCast(node.data_1), } }, - .ty_underscore => return CIR.TypeAnno{ .underscore = {} }, - .ty_ident => return CIR.TypeAnno{ .ty = .{ - .symbol = @bitCast(node.data_1), + .ty_rigid_var_lookup => return CIR.TypeAnno{ .rigid_var_lookup = .{ + .ref = @enumFromInt(node.data_1), } }, + .ty_underscore => return CIR.TypeAnno{ .underscore = {} }, + .ty_lookup => { + const name: Ident.Idx = @bitCast(node.data_1); + const base_enum: CIR.TypeAnno.LocalOrExternal.Tag = @enumFromInt(node.data_2); + + const extra_data = store.extra_data.items.items[node.data_3..]; + const type_base: CIR.TypeAnno.LocalOrExternal = blk: { + switch (base_enum) { + .builtin => { + break :blk .{ .builtin = @enumFromInt(extra_data[0]) }; + }, + .local => { + break :blk .{ .local = .{ .decl_idx = @enumFromInt(extra_data[0]) } }; + }, + .external => { + break :blk .{ .external = .{ + .module_idx = @enumFromInt(extra_data[0]), + .target_node_idx = @intCast(extra_data[1]), + } }; + }, + } + }; + + return CIR.TypeAnno{ .lookup = .{ + .name = name, + .base = type_base, + } }; + }, .ty_tag_union => return CIR.TypeAnno{ .tag_union = .{ .tags = .{ .span = .{ .start = node.data_1, .len = node.data_2 } }, - .ext = if (node.data_3 != 0) @enumFromInt(node.data_3) else null, + .ext = if (node.data_3 != 0) @enumFromInt(node.data_3 - OPTIONAL_VALUE_OFFSET) else null, + } }, + .ty_tag => return CIR.TypeAnno{ .tag = .{ + .name = @bitCast(node.data_1), + .args = .{ .span = .{ .start = node.data_2, .len = node.data_3 } }, } }, .ty_tuple => return CIR.TypeAnno{ .tuple = .{ .elems = .{ .span = .{ .start = node.data_1, .len = node.data_2 } }, } }, - .ty_record => return CIR.TypeAnno{ .record = .{ - .fields = .{ .span = .{ .start = node.data_1, .len = node.data_2 } }, - } }, + .ty_record => return CIR.TypeAnno{ + .record = .{ + .fields = .{ .span = .{ .start = node.data_1, .len = node.data_2 } }, + .ext = if (node.data_3 != 0) @enumFromInt(node.data_3 - OPTIONAL_VALUE_OFFSET) else null, + }, + }, .ty_fn => { const extra_data_idx = node.data_3; const effectful = store.extra_data.items.items[extra_data_idx] != 0; @@ -1002,12 +1241,6 @@ pub fn getTypeAnno(store: *const NodeStore, typeAnno: CIR.TypeAnno.Idx) CIR.Type .ty_parens => return CIR.TypeAnno{ .parens = .{ .anno = @enumFromInt(node.data_1), } }, - .ty_lookup_external => return CIR.TypeAnno{ - .ty_lookup_external = .{ - .module_idx = @enumFromInt(node.data_1), - .target_node_idx = @intCast(node.data_2), - }, - }, .ty_malformed => return CIR.TypeAnno{ .malformed = .{ .diagnostic = @enumFromInt(node.data_1), } }, @@ -1027,9 +1260,15 @@ pub fn getTypeHeader(store: *const NodeStore, typeHeader: CIR.TypeHeader.Idx) CI std.debug.assert(node.tag == .type_header); + // Unpack args from packed format (start in upper 16 bits, len in lower 16 bits) + const packed_args = node.data_3; + const args_start: u32 = packed_args >> 16; + const args_len: u32 = packed_args & 0xFFFF; + return CIR.TypeHeader{ .name = @bitCast(node.data_1), - .args = .{ .span = .{ .start = node.data_2, .len = node.data_3 } }, + .relative_name = @bitCast(node.data_2), + .args = .{ .span = .{ .start = args_start, .len = args_len } }, }; } @@ -1050,9 +1289,21 @@ pub fn getAnnotation(store: *const NodeStore, annotation: CIR.Annotation.Idx) CI std.debug.assert(node.tag == .annotation); + const anno: CIR.TypeAnno.Idx = @enumFromInt(node.data_1); + + const where_flag = node.data_2; + const where_clause = if (where_flag == 1) blk: { + const extra_start = node.data_3; + const extra_data = store.extra_data.items.items[extra_start..]; + + const where_start = extra_data[0]; + const where_len = extra_data[1]; + break :blk CIR.WhereClause.Span{ .span = DataSpan.init(where_start, where_len) }; + } else null; + return CIR.Annotation{ - .type_anno = @enumFromInt(node.data_2), - .signature = @enumFromInt(node.data_1), + .anno = anno, + .where = where_clause, }; } @@ -1077,7 +1328,30 @@ pub fn getExposedItem(store: *const NodeStore, exposedItem: CIR.ExposedItem.Idx) /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addStatement(store: *NodeStore, statement: CIR.Statement, region: base.Region) std.mem.Allocator.Error!CIR.Statement.Idx { +pub fn addStatement(store: *NodeStore, statement: CIR.Statement, region: base.Region) Allocator.Error!CIR.Statement.Idx { + const node = try store.makeStatementNode(statement); + const node_idx = try store.nodes.append(store.gpa, node); + _ = try store.regions.append(store.gpa, region); + return @enumFromInt(@intFromEnum(node_idx)); +} + +/// Set a statement idx to the provided statement +/// +/// This is used when defininig recursive type declarations: +/// 1. Make the placeholder node +/// 2. Introduce to scope +/// 3. Canonicalize the annotation +/// 4. Update the placeholder node with the actual annotation +pub fn setStatementNode(store: *NodeStore, stmt_idx: CIR.Statement.Idx, statement: CIR.Statement) Allocator.Error!void { + const node = try store.makeStatementNode(statement); + store.nodes.set(@enumFromInt(@intFromEnum(stmt_idx)), node); +} + +/// Creates a statement node, but does not append to the store. +/// IMPORTANT: It *does* append to extra_data though +/// +/// See `setStatementNode` to see why this exists +fn makeStatementNode(store: *NodeStore, statement: CIR.Statement) Allocator.Error!Node { var node = Node{ .data_1 = 0, .data_2 = 0, @@ -1100,6 +1374,20 @@ pub fn addStatement(store: *NodeStore, statement: CIR.Statement, region: base.Re node.data_2 = @intFromEnum(s.expr); node.data_3 = extra_data_start; }, + .s_decl_gen => |s| { + const extra_data_start: u32 = @intCast(store.extra_data.len()); + if (s.anno) |anno| { + _ = try store.extra_data.append(store.gpa, @intFromBool(true)); + _ = try store.extra_data.append(store.gpa, @intFromEnum(anno)); + } else { + _ = try store.extra_data.append(store.gpa, @intFromBool(false)); + } + + node.tag = .statement_decl_gen; + node.data_1 = @intFromEnum(s.pattern); + node.data_2 = @intFromEnum(s.expr); + node.data_3 = extra_data_start; + }, .s_var => |s| { node.tag = .statement_var; node.data_1 = @intFromEnum(s.pattern_idx); @@ -1132,9 +1420,16 @@ pub fn addStatement(store: *NodeStore, statement: CIR.Statement, region: base.Re node.data_2 = @intFromEnum(s.expr); node.data_3 = @intFromEnum(s.body); }, + .s_while => |s| { + node.tag = .statement_while; + node.data_1 = @intFromEnum(s.cond); + node.data_2 = @intFromEnum(s.body); + }, .s_return => |s| { node.tag = .statement_return; node.data_1 = @intFromEnum(s.expr); + // Store lambda as data_2, using 0 for null and idx+1 for valid indices + node.data_2 = if (s.lambda) |lambda| @intFromEnum(lambda) + 1 else 0; }, .s_import => |s| { node.tag = .statement_import; @@ -1176,6 +1471,10 @@ pub fn addStatement(store: *NodeStore, statement: CIR.Statement, region: base.Re node.tag = .statement_nominal_decl; node.data_1 = @intFromEnum(s.header); node.data_2 = @intFromEnum(s.anno); + // Store is_opaque in extra_data + const extra_idx = store.extra_data.len(); + _ = try store.extra_data.append(store.gpa, if (s.is_opaque) 1 else 0); + node.data_3 = @intCast(extra_idx); }, .s_type_anno => |s| { node.tag = .statement_type_anno; @@ -1202,27 +1501,31 @@ pub fn addStatement(store: *NodeStore, statement: CIR.Statement, region: base.Re // Store the extra data start position in the node node.data_1 = @intCast(extra_start); }, + .s_type_var_alias => |s| { + node.tag = .statement_type_var_alias; + node.data_1 = @bitCast(s.alias_name); + node.data_2 = @bitCast(s.type_var_name); + node.data_3 = @intFromEnum(s.type_var_anno); + }, .s_runtime_error => |s| { node.data_1 = @intFromEnum(s.diagnostic); node.tag = .malformed; }, } - const node_idx = try store.nodes.append(store.gpa, node); - _ = try store.regions.append(store.gpa, region); - return @enumFromInt(@intFromEnum(node_idx)); + return node; } /// Adds an expression node to the store. /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) std.mem.Allocator.Error!CIR.Expr.Idx { +pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) Allocator.Error!CIR.Expr.Idx { var node = Node{ .data_1 = 0, .data_2 = 0, .data_3 = 0, - .tag = @enumFromInt(0), + .tag = undefined, // set below in switch }; switch (expr) { @@ -1236,8 +1539,16 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) std.mem.A node.data_1 = @intFromEnum(e.module_idx); node.data_2 = e.target_node_idx; }, - .e_int => |e| { - node.tag = .expr_int; + .e_lookup_required => |e| { + // For required lookups (platform requires clause), store the index + node.tag = .expr_required_lookup; + node.data_1 = e.requires_idx.toU32(); + }, + .e_num => |e| { + node.tag = .expr_num; + + node.data_1 = @intFromEnum(e.kind); + node.data_2 = @intFromEnum(e.value.kind); // Store i128 value in extra_data const extra_data_start = store.extra_data.len(); @@ -1251,13 +1562,12 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) std.mem.A } // Store the extra_data index in data_1 - node.data_1 = @intCast(extra_data_start); + node.data_3 = @intCast(extra_data_start); }, .e_list => |e| { node.tag = .expr_list; node.data_1 = e.elems.span.start; node.data_2 = e.elems.span.len; - node.data_3 = @intFromEnum(e.elem_var); }, .e_empty_list => |_| { node.tag = .expr_empty_list; @@ -1270,15 +1580,17 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) std.mem.A .e_frac_f32 => |e| { node.tag = Node.Tag.expr_frac_f32; node.data_1 = @bitCast(e.value); + node.data_2 = @intFromBool(e.has_suffix); }, .e_frac_f64 => |e| { node.tag = .expr_frac_f64; const raw: [2]u32 = @bitCast(e.value); node.data_1 = raw[0]; node.data_2 = raw[1]; + node.data_3 = @intFromBool(e.has_suffix); }, - .e_frac_dec => |e| { - node.tag = .expr_frac_dec; + .e_dec => |e| { + node.tag = .expr_dec; // Store the RocDec value in extra_data const extra_data_start = store.extra_data.len(); @@ -1290,15 +1602,17 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) std.mem.A // Store the extra_data index in data_1 node.data_1 = @intCast(extra_data_start); + node.data_2 = @intFromBool(e.has_suffix); }, .e_dec_small => |e| { node.tag = .expr_dec_small; // Pack small dec data into data_1 and data_3 // data_1: numerator (i16) - fits in lower 16 bits - // data_3: denominator_power_of_ten (u8) in lower 8 bits - node.data_1 = @as(u32, @bitCast(@as(i32, e.numerator))); - node.data_3 = @as(u32, e.denominator_power_of_ten); + // data_2: denominator_power_of_ten (u8) in lower 8 bits + node.data_1 = @as(u32, @bitCast(@as(i32, e.value.numerator))); + node.data_2 = @as(u32, e.value.denominator_power_of_ten); + node.data_3 = @intFromBool(e.has_suffix); }, .e_str_segment => |e| { node.tag = .expr_string_segment; @@ -1333,13 +1647,16 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) std.mem.A node.tag = .expr_dot_access; node.data_1 = @intFromEnum(e.receiver); node.data_2 = @bitCast(e.field_name); + // Store extra data: field_name_region (2 u32s) + optional args (1 u32) + node.data_3 = @intCast(store.extra_data.len()); + _ = try store.extra_data.append(store.gpa, e.field_name_region.start.offset); + _ = try store.extra_data.append(store.gpa, e.field_name_region.end.offset); if (e.args) |args| { - // Use PackedDataSpan for efficient storage - FunctionArgs config is good for method call args std.debug.assert(FunctionArgs.canFit(args.span)); const packed_span = FunctionArgs.fromDataSpanUnchecked(args.span); - node.data_3 = packed_span.toU32(); + _ = try store.extra_data.append(store.gpa, packed_span.toU32() + OPTIONAL_VALUE_OFFSET); } else { - node.data_3 = 0; // No args + _ = try store.extra_data.append(store.gpa, 0); } }, .e_runtime_error => |e| { @@ -1357,6 +1674,58 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) std.mem.A .e_ellipsis => |_| { node.tag = .expr_ellipsis; }, + .e_anno_only => |_| { + node.tag = .expr_anno_only; + }, + .e_return => |ret| { + node.tag = .expr_return; + node.data_1 = @intFromEnum(ret.expr); + }, + .e_type_var_dispatch => |tvd| { + node.tag = .expr_type_var_dispatch; + // data_1 = type_var_alias_stmt (Statement.Idx) + // data_2 = method_name (Ident.Idx) + // extra_data: args span start, args span len + node.data_1 = @intFromEnum(tvd.type_var_alias_stmt); + node.data_2 = @bitCast(tvd.method_name); + + const extra_data_start = store.extra_data.len(); + _ = try store.extra_data.append(store.gpa, tvd.args.span.start); + _ = try store.extra_data.append(store.gpa, tvd.args.span.len); + + node.data_3 = @intCast(extra_data_start); + }, + .e_hosted_lambda => |hosted| { + node.tag = .expr_hosted_lambda; + // data_1 = symbol_name (Ident.Idx via @bitCast) + // data_2 = index (u32) + // extra_data: args span start, args span len, body index + node.data_1 = @bitCast(hosted.symbol_name); + node.data_2 = hosted.index; + + const extra_data_start = store.extra_data.len(); + _ = try store.extra_data.append(store.gpa, hosted.args.span.start); + _ = try store.extra_data.append(store.gpa, hosted.args.span.len); + _ = try store.extra_data.append(store.gpa, @intFromEnum(hosted.body)); + + node.data_3 = @intCast(extra_data_start); + }, + .e_low_level_lambda => |low_level| { + node.tag = .expr_low_level; + node.data_1 = @intFromEnum(low_level.op); + + // Store low-level lambda data in extra_data + const extra_data_start = store.extra_data.len(); + + // Store args span start + _ = try store.extra_data.append(store.gpa, low_level.args.span.start); + // Store args span length + _ = try store.extra_data.append(store.gpa, low_level.args.span.len); + // Store body index + _ = try store.extra_data.append(store.gpa, @intFromEnum(low_level.body)); + + node.data_2 = @intCast(extra_data_start); + }, .e_match => |e| { node.tag = .expr_match; @@ -1396,8 +1765,9 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) std.mem.A // Store args span length _ = try store.extra_data.append(store.gpa, e.args.span.len); - node.data_1 = @intCast(extra_data_start); - node.data_2 = @intFromEnum(e.called_via); + node.data_1 = @intFromEnum(e.func); + node.data_2 = @intCast(extra_data_start); + node.data_3 = @intFromEnum(e.called_via); }, .e_record => |e| { node.tag = .expr_record; @@ -1479,6 +1849,12 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) std.mem.A node.tag = .expr_expect; node.data_1 = @intFromEnum(e.body); }, + .e_for => |e| { + node.tag = .expr_for; + node.data_1 = @intFromEnum(e.patt); + node.data_2 = @intFromEnum(e.expr); + node.data_3 = @intFromEnum(e.body); + }, } const node_idx = try store.nodes.append(store.gpa, node); @@ -1495,7 +1871,7 @@ pub fn addExpr(store: *NodeStore, expr: CIR.Expr, region: base.Region) std.mem.A /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addRecordField(store: *NodeStore, recordField: CIR.RecordField, region: base.Region) std.mem.Allocator.Error!CIR.RecordField.Idx { +pub fn addRecordField(store: *NodeStore, recordField: CIR.RecordField, region: base.Region) Allocator.Error!CIR.RecordField.Idx { const node = Node{ .data_1 = @bitCast(recordField.name), .data_2 = @intFromEnum(recordField.value), @@ -1512,7 +1888,7 @@ pub fn addRecordField(store: *NodeStore, recordField: CIR.RecordField, region: b /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addRecordDestruct(store: *NodeStore, record_destruct: CIR.Pattern.RecordDestruct, region: base.Region) std.mem.Allocator.Error!CIR.Pattern.RecordDestruct.Idx { +pub fn addRecordDestruct(store: *NodeStore, record_destruct: CIR.Pattern.RecordDestruct, region: base.Region) Allocator.Error!CIR.Pattern.RecordDestruct.Idx { const extra_data_start = @as(u32, @intCast(store.extra_data.len())); const node = Node{ .data_1 = @bitCast(record_destruct.label), @@ -1529,11 +1905,11 @@ pub fn addRecordDestruct(store: *NodeStore, record_destruct: CIR.Pattern.RecordD // Store pattern index _ = try store.extra_data.append(store.gpa, @intFromEnum(pattern_idx)); }, - .SubPattern => |sub_pattern| { + .SubPattern => |pattern_idx| { // Store kind tag (1 for SubPattern) _ = try store.extra_data.append(store.gpa, 1); // Store sub-pattern index - _ = try store.extra_data.append(store.gpa, @intFromEnum(sub_pattern)); + _ = try store.extra_data.append(store.gpa, @intFromEnum(pattern_idx)); }, } @@ -1546,7 +1922,7 @@ pub fn addRecordDestruct(store: *NodeStore, record_destruct: CIR.Pattern.RecordD /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addCapture(store: *NodeStore, capture: CIR.Expr.Capture, region: base.Region) std.mem.Allocator.Error!CIR.Expr.Capture.Idx { +pub fn addCapture(store: *NodeStore, capture: CIR.Expr.Capture, region: base.Region) Allocator.Error!CIR.Expr.Capture.Idx { const node = Node{ .tag = .lambda_capture, .data_1 = @bitCast(capture.name), @@ -1563,7 +1939,7 @@ pub fn addCapture(store: *NodeStore, capture: CIR.Expr.Capture, region: base.Reg /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addMatchBranch(store: *NodeStore, branch: CIR.Expr.Match.Branch, region: base.Region) std.mem.Allocator.Error!CIR.Expr.Match.Branch.Idx { +pub fn addMatchBranch(store: *NodeStore, branch: CIR.Expr.Match.Branch, region: base.Region) Allocator.Error!CIR.Expr.Match.Branch.Idx { var node = Node{ .data_1 = 0, .data_2 = 0, @@ -1590,7 +1966,7 @@ pub fn addMatchBranch(store: *NodeStore, branch: CIR.Expr.Match.Branch, region: /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addMatchBranchPattern(store: *NodeStore, branchPattern: CIR.Expr.Match.BranchPattern, region: base.Region) std.mem.Allocator.Error!CIR.Expr.Match.BranchPattern.Idx { +pub fn addMatchBranchPattern(store: *NodeStore, branchPattern: CIR.Expr.Match.BranchPattern, region: base.Region) Allocator.Error!CIR.Expr.Match.BranchPattern.Idx { const node = Node{ .data_1 = @intFromEnum(branchPattern.pattern), .data_2 = @as(u32, @intFromBool(branchPattern.degenerate)), @@ -1606,44 +1982,42 @@ pub fn addMatchBranchPattern(store: *NodeStore, branchPattern: CIR.Expr.Match.Br /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addWhereClause(store: *NodeStore, whereClause: CIR.WhereClause, region: base.Region) std.mem.Allocator.Error!CIR.WhereClause.Idx { +pub fn addWhereClause(store: *NodeStore, whereClause: CIR.WhereClause, region: base.Region) Allocator.Error!CIR.WhereClause.Idx { var node = Node{ .data_1 = 0, .data_2 = 0, .data_3 = 0, - .tag = .where_clause, + .tag = undefined, }; // Store where clause data in extra_data const extra_data_start = store.extra_data.len(); switch (whereClause) { - .mod_method => |mod_method| { - // Store discriminant (0 for mod_method) - _ = try store.extra_data.append(store.gpa, 0); - _ = try store.extra_data.append(store.gpa, @bitCast(mod_method.var_name)); - _ = try store.extra_data.append(store.gpa, @bitCast(mod_method.method_name)); - _ = try store.extra_data.append(store.gpa, mod_method.args.span.start); - _ = try store.extra_data.append(store.gpa, mod_method.args.span.len); - _ = try store.extra_data.append(store.gpa, @intFromEnum(mod_method.ret_anno)); - _ = try store.extra_data.append(store.gpa, @intFromEnum(mod_method.external_decl)); + .w_method => |where_method| { + node.tag = .where_method; + + node.data_1 = @intFromEnum(where_method.var_); + node.data_2 = @bitCast(where_method.method_name); + node.data_3 = @intCast(extra_data_start); + + _ = try store.extra_data.append(store.gpa, where_method.args.span.start); + _ = try store.extra_data.append(store.gpa, where_method.args.span.len); + _ = try store.extra_data.append(store.gpa, @intFromEnum(where_method.ret)); }, - .mod_alias => |mod_alias| { - // Store discriminant (1 for mod_alias) - _ = try store.extra_data.append(store.gpa, 1); - _ = try store.extra_data.append(store.gpa, @bitCast(mod_alias.var_name)); - _ = try store.extra_data.append(store.gpa, @bitCast(mod_alias.alias_name)); - _ = try store.extra_data.append(store.gpa, @intFromEnum(mod_alias.external_decl)); + .w_alias => |mod_alias| { + node.tag = .where_alias; + + node.data_1 = @intFromEnum(mod_alias.var_); + node.data_2 = @bitCast(mod_alias.alias_name); }, - .malformed => |malformed| { - // Store discriminant (2 for malformed) - _ = try store.extra_data.append(store.gpa, 2); - _ = try store.extra_data.append(store.gpa, @intFromEnum(malformed.diagnostic)); + .w_malformed => |malformed| { + node.tag = .where_malformed; + + node.data_1 = @intFromEnum(malformed.diagnostic); }, } - node.data_1 = @intCast(extra_data_start); - const nid = try store.nodes.append(store.gpa, node); _ = try store.regions.append(store.gpa, region); return @enumFromInt(@intFromEnum(nid)); @@ -1653,12 +2027,12 @@ pub fn addWhereClause(store: *NodeStore, whereClause: CIR.WhereClause, region: b /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addPattern(store: *NodeStore, pattern: CIR.Pattern, region: base.Region) std.mem.Allocator.Error!CIR.Pattern.Idx { +pub fn addPattern(store: *NodeStore, pattern: CIR.Pattern, region: base.Region) Allocator.Error!CIR.Pattern.Idx { var node = Node{ .data_1 = 0, .data_2 = 0, .data_3 = 0, - .tag = @enumFromInt(0), + .tag = undefined, }; switch (pattern) { @@ -1694,13 +2068,8 @@ pub fn addPattern(store: *NodeStore, pattern: CIR.Pattern, region: base.Region) .record_destructure => |p| { node.tag = .pattern_record_destructure; - // Store record destructure data in extra_data - const extra_data_start = store.extra_data.len(); - _ = try store.extra_data.append(store.gpa, p.destructs.span.start); - _ = try store.extra_data.append(store.gpa, p.destructs.span.len); - _ = try store.extra_data.append(store.gpa, @intFromEnum(p.ext_var)); - _ = try store.extra_data.append(store.gpa, @intFromEnum(p.whole_var)); - node.data_1 = @intCast(extra_data_start); + node.data_1 = p.destructs.span.start; + node.data_2 = p.destructs.span.len; }, .list => |p| { node.tag = .pattern_list; @@ -1709,8 +2078,6 @@ pub fn addPattern(store: *NodeStore, pattern: CIR.Pattern, region: base.Region) const extra_data_start = store.extra_data.len(); _ = try store.extra_data.append(store.gpa, p.patterns.span.start); _ = try store.extra_data.append(store.gpa, p.patterns.span.len); - _ = try store.extra_data.append(store.gpa, @intFromEnum(p.elem_var)); - _ = try store.extra_data.append(store.gpa, @intFromEnum(p.list_var)); // Store rest_info if (p.rest_info) |rest| { @@ -1733,23 +2100,26 @@ pub fn addPattern(store: *NodeStore, pattern: CIR.Pattern, region: base.Region) node.data_1 = p.patterns.span.start; node.data_2 = p.patterns.span.len; }, - .int_literal => |p| { - node.tag = .pattern_int_literal; - // Store the value in extra_data - const extra_data_start = store.extra_data.len(); + .num_literal => |p| { + node.tag = .pattern_num_literal; + + node.data_1 = @intFromEnum(p.kind); + node.data_2 = @intFromEnum(p.value.kind); + node.data_3 = @intCast(store.extra_data.len()); + const value_as_u32s: [4]u32 = @bitCast(p.value.bytes); for (value_as_u32s) |word| { _ = try store.extra_data.append(store.gpa, word); } - node.data_1 = @intCast(extra_data_start); }, .small_dec_literal => |p| { node.tag = .pattern_small_dec_literal; // Pack small dec data into data_1 and data_3 // data_1: numerator (i16) - fits in lower 16 bits // data_3: denominator_power_of_ten (u8) in lower 8 bits - node.data_1 = @as(u32, @bitCast(@as(i32, p.numerator))); - node.data_3 = @as(u32, p.denominator_power_of_ten); + node.data_1 = @as(u32, @bitCast(@as(i32, p.value.numerator))); + node.data_2 = @as(u32, p.value.denominator_power_of_ten); + node.data_3 = @intFromBool(p.has_suffix); }, .dec_literal => |p| { node.tag = .pattern_dec_literal; @@ -1760,6 +2130,7 @@ pub fn addPattern(store: *NodeStore, pattern: CIR.Pattern, region: base.Region) _ = try store.extra_data.append(store.gpa, word); } node.data_1 = @intCast(extra_data_start); + node.data_2 = @intFromBool(p.has_suffix); }, .str_literal => |p| { node.tag = .pattern_str_literal; @@ -1791,59 +2162,87 @@ pub fn addPattern(store: *NodeStore, pattern: CIR.Pattern, region: base.Region) return @enumFromInt(@intFromEnum(node_idx)); } -/// Adds a pattern record field to the store. -pub fn addPatternRecordField(store: *NodeStore, patternRecordField: CIR.PatternRecordField) std.mem.Allocator.Error!CIR.PatternRecordField.Idx { - _ = store; - _ = patternRecordField; - - return @enumFromInt(0); -} - /// Adds a type annotation to the store. /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addTypeAnno(store: *NodeStore, typeAnno: CIR.TypeAnno, region: base.Region) std.mem.Allocator.Error!CIR.TypeAnno.Idx { +pub fn addTypeAnno(store: *NodeStore, typeAnno: CIR.TypeAnno, region: base.Region) Allocator.Error!CIR.TypeAnno.Idx { var node = Node{ .data_1 = 0, .data_2 = 0, .data_3 = 0, - .tag = @enumFromInt(0), + .tag = undefined, // set below in switch }; switch (typeAnno) { .apply => |a| { - node.data_1 = @bitCast(a.symbol); + node.data_1 = @bitCast(a.name); node.data_2 = a.args.span.start; - node.data_3 = a.args.span.len; + + const ed_start = store.extra_data.len(); + _ = try store.extra_data.append(store.gpa, a.args.span.len); + _ = try store.extra_data.append(store.gpa, @intFromEnum(@as(CIR.TypeAnno.LocalOrExternal.Tag, std.meta.activeTag(a.base)))); + switch (a.base) { + .builtin => |builtin_type| { + _ = try store.extra_data.append(store.gpa, @intFromEnum(builtin_type)); + }, + .local => |local| { + _ = try store.extra_data.append(store.gpa, @intFromEnum(local.decl_idx)); + }, + .external => |ext| { + _ = try store.extra_data.append(store.gpa, @intFromEnum(ext.module_idx)); + _ = try store.extra_data.append(store.gpa, @intCast(ext.target_node_idx)); + }, + } + + node.data_3 = @intCast(ed_start); + node.tag = .ty_apply; }, - .apply_external => |a| { - node.data_1 = @intFromEnum(a.module_idx); - node.data_2 = a.target_node_idx; - const ed_start = store.extra_data.len(); - _ = try store.extra_data.append(store.gpa, a.args.span.start); - _ = try store.extra_data.append(store.gpa, a.args.span.len); - node.data_3 = @intCast(ed_start); - node.tag = .ty_apply_external; - }, - .ty_var => |tv| { + .rigid_var => |tv| { node.data_1 = @bitCast(tv.name); - node.tag = .ty_var; + node.tag = .ty_rigid_var; + }, + .rigid_var_lookup => |tv| { + node.data_1 = @intFromEnum(tv.ref); + node.tag = .ty_rigid_var_lookup; }, .underscore => |_| { node.tag = .ty_underscore; }, - .ty => |t| { - node.data_1 = @bitCast(t.symbol); - node.tag = .ty_ident; + .lookup => |t| { + node.data_1 = @bitCast(t.name); + node.data_2 = @intFromEnum(@as(CIR.TypeAnno.LocalOrExternal.Tag, std.meta.activeTag(t.base))); + + const ed_start = store.extra_data.len(); + switch (t.base) { + .builtin => |builtin_type| { + _ = try store.extra_data.append(store.gpa, @intFromEnum(builtin_type)); + }, + .local => |local| { + _ = try store.extra_data.append(store.gpa, @intFromEnum(local.decl_idx)); + }, + .external => |ext| { + _ = try store.extra_data.append(store.gpa, @intFromEnum(ext.module_idx)); + _ = try store.extra_data.append(store.gpa, @intCast(ext.target_node_idx)); + }, + } + node.data_3 = @intCast(ed_start); + + node.tag = .ty_lookup; }, .tag_union => |tu| { node.data_1 = tu.tags.span.start; node.data_2 = tu.tags.span.len; - node.data_3 = if (tu.ext) |ext| @intFromEnum(ext) else 0; + node.data_3 = if (tu.ext) |ext| @intFromEnum(ext) + OPTIONAL_VALUE_OFFSET else 0; node.tag = .ty_tag_union; }, + .tag => |t| { + node.data_1 = @bitCast(t.name); + node.data_2 = t.args.span.start; + node.data_3 = t.args.span.len; + node.tag = .ty_tag; + }, .tuple => |t| { node.data_1 = t.elems.span.start; node.data_2 = t.elems.span.len; @@ -1852,6 +2251,7 @@ pub fn addTypeAnno(store: *NodeStore, typeAnno: CIR.TypeAnno, region: base.Regio .record => |r| { node.data_1 = r.fields.span.start; node.data_2 = r.fields.span.len; + node.data_3 = if (r.ext) |ext| @intFromEnum(ext) + OPTIONAL_VALUE_OFFSET else 0; node.tag = .ty_record; }, .@"fn" => |f| { @@ -1867,11 +2267,6 @@ pub fn addTypeAnno(store: *NodeStore, typeAnno: CIR.TypeAnno, region: base.Regio node.data_1 = @intFromEnum(p.anno); node.tag = .ty_parens; }, - .ty_lookup_external => |tle| { - node.tag = .ty_lookup_external; - node.data_1 = @intFromEnum(tle.module_idx); - node.data_2 = tle.target_node_idx; - }, .malformed => |m| { node.data_1 = @intFromEnum(m.diagnostic); node.tag = .ty_malformed; @@ -1887,11 +2282,16 @@ pub fn addTypeAnno(store: *NodeStore, typeAnno: CIR.TypeAnno, region: base.Regio /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addTypeHeader(store: *NodeStore, typeHeader: CIR.TypeHeader, region: base.Region) std.mem.Allocator.Error!CIR.TypeHeader.Idx { +pub fn addTypeHeader(store: *NodeStore, typeHeader: CIR.TypeHeader, region: base.Region) Allocator.Error!CIR.TypeHeader.Idx { + // Pack args.start and args.len into one u32 (16 bits each) to free up data_3 for relative_name + std.debug.assert(typeHeader.args.span.start <= std.math.maxInt(u16)); + std.debug.assert(typeHeader.args.span.len <= std.math.maxInt(u16)); + const packed_args: u32 = (@as(u32, @intCast(typeHeader.args.span.start)) << 16) | @as(u32, @intCast(typeHeader.args.span.len)); + const node = Node{ .data_1 = @bitCast(typeHeader.name), - .data_2 = typeHeader.args.span.start, - .data_3 = typeHeader.args.span.len, + .data_2 = @bitCast(typeHeader.relative_name), + .data_3 = packed_args, .tag = .type_header, }; @@ -1904,7 +2304,7 @@ pub fn addTypeHeader(store: *NodeStore, typeHeader: CIR.TypeHeader, region: base /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addAnnoRecordField(store: *NodeStore, annoRecordField: CIR.TypeAnno.RecordField, region: base.Region) std.mem.Allocator.Error!CIR.TypeAnno.RecordField.Idx { +pub fn addAnnoRecordField(store: *NodeStore, annoRecordField: CIR.TypeAnno.RecordField, region: base.Region) Allocator.Error!CIR.TypeAnno.RecordField.Idx { const node = Node{ .data_1 = @bitCast(annoRecordField.name), .data_2 = @intFromEnum(annoRecordField.ty), @@ -1921,14 +2321,30 @@ pub fn addAnnoRecordField(store: *NodeStore, annoRecordField: CIR.TypeAnno.Recor /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addAnnotation(store: *NodeStore, annotation: CIR.Annotation, region: base.Region) std.mem.Allocator.Error!CIR.Annotation.Idx { - const node = Node{ - .data_1 = @intFromEnum(annotation.signature), - .data_2 = @intFromEnum(annotation.type_anno), +pub fn addAnnotation(store: *NodeStore, annotation: CIR.Annotation, region: base.Region) Allocator.Error!CIR.Annotation.Idx { + var node = Node{ + .data_1 = 0, + .data_2 = 0, .data_3 = 0, .tag = .annotation, }; + node.data_1 = @intFromEnum(annotation.anno); + + // Store where clause information + if (annotation.where) |where_clause| { + // Store flag indicating where clause is present + node.data_2 = 1; + // Store where clause span start and len + const extra_start = store.extra_data.len(); + _ = try store.extra_data.append(store.gpa, where_clause.span.start); + _ = try store.extra_data.append(store.gpa, where_clause.span.len); + node.data_3 = @intCast(extra_start); + } else { + // Store flag indicating where clause is not present + node.data_2 = 0; + } + const nid = try store.nodes.append(store.gpa, node); _ = try store.regions.append(store.gpa, region); return @enumFromInt(@intFromEnum(nid)); @@ -1938,7 +2354,7 @@ pub fn addAnnotation(store: *NodeStore, annotation: CIR.Annotation, region: base /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addExposedItem(store: *NodeStore, exposedItem: CIR.ExposedItem, region: base.Region) std.mem.Allocator.Error!CIR.ExposedItem.Idx { +pub fn addExposedItem(store: *NodeStore, exposedItem: CIR.ExposedItem, region: base.Region) Allocator.Error!CIR.ExposedItem.Idx { const node = Node{ .data_1 = @bitCast(exposedItem.name), .data_2 = if (exposedItem.alias) |alias| @bitCast(alias) else 0, @@ -1955,7 +2371,7 @@ pub fn addExposedItem(store: *NodeStore, exposedItem: CIR.ExposedItem, region: b /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addDef(store: *NodeStore, def: CIR.Def, region: base.Region) std.mem.Allocator.Error!CIR.Def.Idx { +pub fn addDef(store: *NodeStore, def: CIR.Def, region: base.Region) Allocator.Error!CIR.Def.Idx { var node = Node{ .data_1 = 0, .data_2 = 0, @@ -2012,6 +2428,20 @@ pub fn getDef(store: *const NodeStore, def_idx: CIR.Def.Idx) CIR.Def { }; } +/// Updates the expression field of an existing definition. +/// This is used during constant folding to replace expressions with their folded equivalents. +pub fn setDefExpr(store: *NodeStore, def_idx: CIR.Def.Idx, new_expr: CIR.Expr.Idx) void { + const nid: Node.Idx = @enumFromInt(@intFromEnum(def_idx)); + const node = store.nodes.get(nid); + + std.debug.assert(node.tag == .def); + + const extra_start = node.data_1; + // The expr field is at offset 1 in the extra_data layout for Def + // Layout: [0]=pattern, [1]=expr, [2-3]=kind, [4]=annotation + store.extra_data.items.items[extra_start + 1] = @intFromEnum(new_expr); +} + /// Retrieves a capture from the store. pub fn getCapture(store: *const NodeStore, capture_idx: CIR.Expr.Capture.Idx) CIR.Expr.Capture { const nid: Node.Idx = @enumFromInt(@intFromEnum(capture_idx)); @@ -2075,24 +2505,32 @@ pub fn getIfBranch(store: *const NodeStore, if_branch_idx: CIR.Expr.IfBranch.Idx }; } +/// Check if a raw node index refers to a definition node. +/// This is useful when exposed items might be either definitions or type declarations. +pub fn isDefNode(store: *const NodeStore, node_idx: u16) bool { + const nid: Node.Idx = @enumFromInt(node_idx); + const node = store.nodes.get(nid); + return node.tag == .def; +} + /// Generic function to get the top of any scratch buffer pub fn scratchTop(store: *NodeStore, comptime field_name: []const u8) u32 { - return @field(store, field_name).top(); + return @field(store.scratch.?, field_name).top(); } /// Generic function to add an item to any scratch buffer -pub fn addScratch(store: *NodeStore, comptime field_name: []const u8, idx: anytype) std.mem.Allocator.Error!void { - try @field(store, field_name).append(store.gpa, idx); +pub fn addScratch(store: *NodeStore, comptime field_name: []const u8, idx: anytype) Allocator.Error!void { + try @field(store.scratch.?, field_name).append(idx); } /// Generic function to clear any scratch buffer from a given position pub fn clearScratchFrom(store: *NodeStore, comptime field_name: []const u8, start: u32) void { - @field(store, field_name).clearFrom(start); + @field(store.scratch.?, field_name).clearFrom(start); } /// Generic function to create a span from any scratch buffer -pub fn spanFrom(store: *NodeStore, comptime field_name: []const u8, comptime SpanType: type, start: u32) std.mem.Allocator.Error!SpanType { - const scratch_field = &@field(store, field_name); +pub fn spanFrom(store: *NodeStore, comptime field_name: []const u8, comptime SpanType: type, start: u32) Allocator.Error!SpanType { + const scratch_field = &@field(store.scratch.?, field_name); const end = scratch_field.top(); defer scratch_field.clearFrom(start); var i = @as(usize, @intCast(start)); @@ -2107,42 +2545,42 @@ pub fn spanFrom(store: *NodeStore, comptime field_name: []const u8, comptime Spa /// Returns the top of the scratch expressions buffer. pub fn scratchExprTop(store: *NodeStore) u32 { - return store.scratchTop("scratch_exprs"); + return store.scratchTop("exprs"); } /// Adds a scratch expression to temporary storage. -pub fn addScratchExpr(store: *NodeStore, idx: CIR.Expr.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_exprs", idx); +pub fn addScratchExpr(store: *NodeStore, idx: CIR.Expr.Idx) Allocator.Error!void { + try store.addScratch("exprs", idx); } /// Adds a capture index to the scratch captures list for building spans. -pub fn addScratchCapture(store: *NodeStore, idx: CIR.Expr.Capture.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_captures", idx); +pub fn addScratchCapture(store: *NodeStore, idx: CIR.Expr.Capture.Idx) Allocator.Error!void { + try store.addScratch("captures", idx); } /// Adds a statement index to the scratch statements list for building spans. -pub fn addScratchStatement(store: *NodeStore, idx: CIR.Statement.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_statements", idx); +pub fn addScratchStatement(store: *NodeStore, idx: CIR.Statement.Idx) Allocator.Error!void { + try store.addScratch("statements", idx); } /// Computes the span of an expression starting from a given index. -pub fn exprSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.Expr.Span { - return try store.spanFrom("scratch_exprs", CIR.Expr.Span, start); +pub fn exprSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.Expr.Span { + return try store.spanFrom("exprs", CIR.Expr.Span, start); } /// Computes the span of captures starting from a given index. -pub fn capturesSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.Expr.Capture.Span { - return try store.spanFrom("scratch_captures", CIR.Expr.Capture.Span, start); +pub fn capturesSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.Expr.Capture.Span { + return try store.spanFrom("captures", CIR.Expr.Capture.Span, start); } /// Creates a statement span from the given start position to the current top of scratch statements. -pub fn statementSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.Statement.Span { - return try store.spanFrom("scratch_statements", CIR.Statement.Span, start); +pub fn statementSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.Statement.Span { + return try store.spanFrom("statements", CIR.Statement.Span, start); } /// Clears scratch expressions starting from a specified index. pub fn clearScratchExprsFrom(store: *NodeStore, start: u32) void { - store.clearScratchFrom("scratch_exprs", start); + store.clearScratchFrom("exprs", start); } /// Returns a slice of expressions from the scratch space. @@ -2152,97 +2590,97 @@ pub fn exprSlice(store: *const NodeStore, span: CIR.Expr.Span) []CIR.Expr.Idx { /// Returns the top index for scratch definitions. pub fn scratchDefTop(store: *NodeStore) u32 { - return store.scratchTop("scratch_defs"); + return store.scratchTop("defs"); } /// Adds a scratch definition to temporary storage. -pub fn addScratchDef(store: *NodeStore, idx: CIR.Def.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_defs", idx); +pub fn addScratchDef(store: *NodeStore, idx: CIR.Def.Idx) Allocator.Error!void { + try store.addScratch("defs", idx); } /// Adds a type annotation to the scratch buffer. -pub fn addScratchTypeAnno(store: *NodeStore, idx: CIR.TypeAnno.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_type_annos", idx); +pub fn addScratchTypeAnno(store: *NodeStore, idx: CIR.TypeAnno.Idx) Allocator.Error!void { + try store.addScratch("type_annos", idx); } /// Adds a where clause to the scratch buffer. -pub fn addScratchWhereClause(store: *NodeStore, idx: CIR.WhereClause.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_where_clauses", idx); +pub fn addScratchWhereClause(store: *NodeStore, idx: CIR.WhereClause.Idx) Allocator.Error!void { + try store.addScratch("where_clauses", idx); } /// Returns the current top of the scratch type annotations buffer. pub fn scratchTypeAnnoTop(store: *NodeStore) u32 { - return store.scratchTop("scratch_type_annos"); + return store.scratchTop("type_annos"); } /// Returns the current top of the scratch where clauses buffer. pub fn scratchWhereClauseTop(store: *NodeStore) u32 { - return store.scratchTop("scratch_where_clauses"); + return store.scratchTop("where_clauses"); } /// Clears scratch type annotations from the given index. pub fn clearScratchTypeAnnosFrom(store: *NodeStore, from: u32) void { - store.clearScratchFrom("scratch_type_annos", from); + store.clearScratchFrom("type_annos", from); } /// Clears scratch where clauses from the given index. pub fn clearScratchWhereClausesFrom(store: *NodeStore, from: u32) void { - store.clearScratchFrom("scratch_where_clauses", from); + store.clearScratchFrom("where_clauses", from); } /// Creates a span from the scratch type annotations starting at the given index. -pub fn typeAnnoSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.TypeAnno.Span { - return try store.spanFrom("scratch_type_annos", CIR.TypeAnno.Span, start); +pub fn typeAnnoSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.TypeAnno.Span { + return try store.spanFrom("type_annos", CIR.TypeAnno.Span, start); } /// Returns a span from the scratch anno record fields starting at the given index. -pub fn annoRecordFieldSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.TypeAnno.RecordField.Span { - return try store.spanFrom("scratch_anno_record_fields", CIR.TypeAnno.RecordField.Span, start); +pub fn annoRecordFieldSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.TypeAnno.RecordField.Span { + return try store.spanFrom("anno_record_fields", CIR.TypeAnno.RecordField.Span, start); } /// Returns a span from the scratch record fields starting at the given index. -pub fn recordFieldSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.RecordField.Span { - return try store.spanFrom("scratch_record_fields", CIR.RecordField.Span, start); +pub fn recordFieldSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.RecordField.Span { + return try store.spanFrom("record_fields", CIR.RecordField.Span, start); } /// Returns a span from the scratch where clauses starting at the given index. -pub fn whereClauseSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.WhereClause.Span { - return try store.spanFrom("scratch_where_clauses", CIR.WhereClause.Span, start); +pub fn whereClauseSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.WhereClause.Span { + return try store.spanFrom("where_clauses", CIR.WhereClause.Span, start); } /// Returns the current top of the scratch exposed items buffer. pub fn scratchExposedItemTop(store: *NodeStore) u32 { - return store.scratchTop("scratch_exposed_items"); + return store.scratchTop("exposed_items"); } /// Adds an exposed item to the scratch buffer. -pub fn addScratchExposedItem(store: *NodeStore, idx: CIR.ExposedItem.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_exposed_items", idx); +pub fn addScratchExposedItem(store: *NodeStore, idx: CIR.ExposedItem.Idx) Allocator.Error!void { + try store.addScratch("exposed_items", idx); } /// Creates a span from the scratch exposed items starting at the given index. -pub fn exposedItemSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.ExposedItem.Span { - return try store.spanFrom("scratch_exposed_items", CIR.ExposedItem.Span, start); +pub fn exposedItemSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.ExposedItem.Span { + return try store.spanFrom("exposed_items", CIR.ExposedItem.Span, start); } /// Clears scratch exposed items from the given index. pub fn clearScratchExposedItemsFrom(store: *NodeStore, start: u32) void { - store.clearScratchFrom("scratch_exposed_items", start); + store.clearScratchFrom("exposed_items", start); } /// Returns the start position for a new Span of annoRecordFieldIdxs in scratch pub fn scratchAnnoRecordFieldTop(store: *NodeStore) u32 { - return store.scratchTop("scratch_anno_record_fields"); + return store.scratchTop("anno_record_fields"); } /// Places a new CIR.TypeAnno.RecordField.Idx in the scratch. Will panic on OOM. -pub fn addScratchAnnoRecordField(store: *NodeStore, idx: CIR.TypeAnno.RecordField.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_anno_record_fields", idx); +pub fn addScratchAnnoRecordField(store: *NodeStore, idx: CIR.TypeAnno.RecordField.Idx) Allocator.Error!void { + try store.addScratch("anno_record_fields", idx); } /// Clears any AnnoRecordFieldIds added to scratch from start until the end. pub fn clearScratchAnnoRecordFieldsFrom(store: *NodeStore, start: u32) void { - store.clearScratchFrom("scratch_anno_record_fields", start); + store.clearScratchFrom("anno_record_fields", start); } /// Returns a new AnnoRecordField slice so that the caller can iterate through @@ -2252,43 +2690,43 @@ pub fn annoRecordFieldSlice(store: *NodeStore, span: CIR.TypeAnno.RecordField.Sp } /// Computes the span of a definition starting from a given index. -pub fn defSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.Def.Span { - return try store.spanFrom("scratch_defs", CIR.Def.Span, start); +pub fn defSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.Def.Span { + return try store.spanFrom("defs", CIR.Def.Span, start); } /// Retrieves a slice of record destructures from the store. -pub fn recordDestructSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.Pattern.RecordDestruct.Span { - return try store.spanFrom("scratch_record_destructs", CIR.Pattern.RecordDestruct.Span, start); +pub fn recordDestructSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.Pattern.RecordDestruct.Span { + return try store.spanFrom("record_destructs", CIR.Pattern.RecordDestruct.Span, start); } /// Returns the current top of the scratch patterns buffer. pub fn scratchPatternTop(store: *NodeStore) u32 { - return store.scratchTop("scratch_patterns"); + return store.scratchTop("patterns"); } /// Adds a pattern to the scratch patterns list for building spans. -pub fn addScratchPattern(store: *NodeStore, idx: CIR.Pattern.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_patterns", idx); +pub fn addScratchPattern(store: *NodeStore, idx: CIR.Pattern.Idx) Allocator.Error!void { + try store.addScratch("patterns", idx); } /// Returns the current top of the scratch record destructures buffer. pub fn scratchRecordDestructTop(store: *NodeStore) u32 { - return store.scratchTop("scratch_record_destructs"); + return store.scratchTop("record_destructs"); } /// Adds a record destructure to the scratch record destructures list for building spans. -pub fn addScratchRecordDestruct(store: *NodeStore, idx: CIR.Pattern.RecordDestruct.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_record_destructs", idx); +pub fn addScratchRecordDestruct(store: *NodeStore, idx: CIR.Pattern.RecordDestruct.Idx) Allocator.Error!void { + try store.addScratch("record_destructs", idx); } /// Creates a pattern span from the given start position to the current top of scratch patterns. -pub fn patternSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.Pattern.Span { - return try store.spanFrom("scratch_patterns", CIR.Pattern.Span, start); +pub fn patternSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.Pattern.Span { + return try store.spanFrom("patterns", CIR.Pattern.Span, start); } /// Clears scratch definitions starting from a specified index. pub fn clearScratchDefsFrom(store: *NodeStore, start: u32) void { - store.clearScratchFrom("scratch_defs", start); + store.clearScratchFrom("defs", start); } /// Creates a slice corresponding to a span. @@ -2363,24 +2801,24 @@ pub fn lastFromStatements(store: *const NodeStore, span: CIR.Statement.Span) CIR /// Returns a slice of if branches from the store. pub fn scratchIfBranchTop(store: *NodeStore) u32 { - return store.scratchTop("scratch_if_branches"); + return store.scratchTop("if_branches"); } /// Adds an if branch to the scratch if branches list for building spans. -pub fn addScratchIfBranch(store: *NodeStore, if_branch_idx: CIR.Expr.IfBranch.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_if_branches", if_branch_idx); +pub fn addScratchIfBranch(store: *NodeStore, if_branch_idx: CIR.Expr.IfBranch.Idx) Allocator.Error!void { + try store.addScratch("if_branches", if_branch_idx); } /// Creates an if branch span from the given start position to the current top of scratch if branches. -pub fn ifBranchSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.Expr.IfBranch.Span { - return try store.spanFrom("scratch_if_branches", CIR.Expr.IfBranch.Span, start); +pub fn ifBranchSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.Expr.IfBranch.Span { + return try store.spanFrom("if_branches", CIR.Expr.IfBranch.Span, start); } /// Adds an if branch to the store and returns its index. /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addIfBranch(store: *NodeStore, if_branch: CIR.Expr.IfBranch, region: base.Region) std.mem.Allocator.Error!CIR.Expr.IfBranch.Idx { +pub fn addIfBranch(store: *NodeStore, if_branch: CIR.Expr.IfBranch, region: base.Region) Allocator.Error!CIR.Expr.IfBranch.Idx { const node = Node{ .data_1 = @intFromEnum(if_branch.cond), .data_2 = @intFromEnum(if_branch.body), @@ -2434,12 +2872,12 @@ pub fn sliceRecordDestructs(store: *const NodeStore, span: CIR.Pattern.RecordDes /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) std.mem.Allocator.Error!CIR.Diagnostic.Idx { +pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) Allocator.Error!CIR.Diagnostic.Idx { var node = Node{ .data_1 = 0, .data_2 = 0, .data_3 = 0, - .tag = @enumFromInt(0), + .tag = undefined, // set below in switch }; var region = base.Region.zero(); @@ -2453,10 +2891,6 @@ pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) std.mem.Allocato node.tag = .diag_invalid_num_literal; region = r.region; }, - .invalid_single_quote => |r| { - node.tag = .diag_invalid_single_quote; - region = r.region; - }, .empty_tuple => |r| { node.tag = .diag_empty_tuple; region = r.region; @@ -2487,6 +2921,11 @@ pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) std.mem.Allocato region = r.region; node.data_1 = @bitCast(r.ident); }, + .qualified_ident_does_not_exist => |r| { + node.tag = .diag_qualified_ident_does_not_exist; + region = r.region; + node.data_1 = @bitCast(r.ident); + }, .invalid_top_level_statement => |r| { node.tag = .diag_invalid_top_level_statement; node.data_1 = @intFromEnum(r.stmt); @@ -2528,6 +2967,10 @@ pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) std.mem.Allocato node.tag = .diag_if_else_not_canonicalized; region = r.region; }, + .if_expr_without_else => |r| { + node.tag = .diag_if_expr_without_else; + region = r.region; + }, .malformed_type_annotation => |r| { node.tag = .diag_malformed_type_annotation; region = r.region; @@ -2540,6 +2983,52 @@ pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) std.mem.Allocato node.tag = .diag_where_clause_not_allowed_in_type_decl; region = r.region; }, + .type_module_missing_matching_type => |r| { + node.tag = .diag_type_module_missing_matching_type; + region = r.region; + node.data_1 = @bitCast(r.module_name); + }, + .default_app_missing_main => |r| { + node.tag = .diag_default_app_missing_main; + region = r.region; + node.data_1 = @bitCast(r.module_name); + }, + .default_app_wrong_arity => |r| { + node.tag = .diag_default_app_wrong_arity; + region = r.region; + node.data_1 = r.arity; + }, + .cannot_import_default_app => |r| { + node.tag = .diag_cannot_import_default_app; + region = r.region; + node.data_1 = @bitCast(r.module_name); + }, + .execution_requires_app_or_default_app => |r| { + node.tag = .diag_execution_requires_app_or_default_app; + region = r.region; + }, + .type_name_case_mismatch => |r| { + node.tag = .diag_type_name_case_mismatch; + region = r.region; + node.data_1 = @bitCast(r.module_name); + node.data_2 = @bitCast(r.type_name); + }, + .module_header_deprecated => |r| { + node.tag = .diag_module_header_deprecated; + region = r.region; + }, + .redundant_expose_main_type => |r| { + node.tag = .diag_redundant_expose_main_type; + region = r.region; + node.data_1 = @bitCast(r.type_name); + node.data_2 = @bitCast(r.module_name); + }, + .invalid_main_type_rename_in_exposing => |r| { + node.tag = .diag_invalid_main_type_rename_in_exposing; + region = r.region; + node.data_1 = @bitCast(r.type_name); + node.data_2 = @bitCast(r.alias); + }, .var_across_function_boundary => |r| { node.tag = .diag_var_across_function_boundary; region = r.region; @@ -2601,11 +3090,29 @@ pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) std.mem.Allocato node.data_1 = @as(u32, @bitCast(r.module_name)); node.data_2 = @as(u32, @bitCast(r.type_name)); }, + .type_from_missing_module => |r| { + node.tag = .diag_type_from_missing_module; + region = r.region; + node.data_1 = @as(u32, @bitCast(r.module_name)); + node.data_2 = @as(u32, @bitCast(r.type_name)); + }, .module_not_imported => |r| { node.tag = .diag_module_not_imported; region = r.region; node.data_1 = @as(u32, @bitCast(r.module_name)); }, + .nested_type_not_found => |r| { + node.tag = .diag_nested_type_not_found; + region = r.region; + node.data_1 = @as(u32, @bitCast(r.parent_name)); + node.data_2 = @as(u32, @bitCast(r.nested_name)); + }, + .nested_value_not_found => |r| { + node.tag = .diag_nested_value_not_found; + region = r.region; + node.data_1 = @as(u32, @bitCast(r.parent_name)); + node.data_2 = @as(u32, @bitCast(r.nested_name)); + }, .too_many_exports => |r| { node.tag = .diag_too_many_exports; region = r.region; @@ -2677,8 +3184,8 @@ pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) std.mem.Allocato node.data_1 = @bitCast(r.name); node.data_2 = @bitCast(r.suggested_name); }, - .type_var_ending_in_underscore => |r| { - node.tag = .diag_type_var_ending_in_underscore; + .type_var_starting_with_dollar => |r| { + node.tag = .diag_type_var_starting_with_dollar; region = r.region; node.data_1 = @bitCast(r.name); node.data_2 = @bitCast(r.suggested_name); @@ -2694,7 +3201,7 @@ pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) std.mem.Allocato _ = try store.regions.append(store.gpa, region); // append to our scratch so we can get a span later of all our diagnostics - try store.addScratch("scratch_diagnostics", @as(CIR.Diagnostic.Idx, @enumFromInt(nid))); + try store.addScratch("diagnostics", @as(CIR.Diagnostic.Idx, @enumFromInt(nid))); return @enumFromInt(nid); } @@ -2715,7 +3222,7 @@ pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) std.mem.Allocato /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addMalformed(store: *NodeStore, diagnostic_idx: CIR.Diagnostic.Idx, region: Region) std.mem.Allocator.Error!Node.Idx { +pub fn addMalformed(store: *NodeStore, diagnostic_idx: CIR.Diagnostic.Idx, region: Region) Allocator.Error!Node.Idx { const malformed_node = Node{ .data_1 = @intFromEnum(diagnostic_idx), .data_2 = 0, @@ -2743,9 +3250,6 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI .diag_invalid_num_literal => return CIR.Diagnostic{ .invalid_num_literal = .{ .region = store.getRegionAt(node_idx), } }, - .diag_invalid_single_quote => return CIR.Diagnostic{ .invalid_single_quote = .{ - .region = store.getRegionAt(node_idx), - } }, .diag_empty_tuple => return CIR.Diagnostic{ .empty_tuple = .{ .region = store.getRegionAt(node_idx), } }, @@ -2774,6 +3278,10 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI .ident = @bitCast(node.data_1), .region = store.getRegionAt(node_idx), } }, + .diag_qualified_ident_does_not_exist => return CIR.Diagnostic{ .qualified_ident_does_not_exist = .{ + .ident = @bitCast(node.data_1), + .region = store.getRegionAt(node_idx), + } }, .diag_invalid_top_level_statement => return CIR.Diagnostic{ .invalid_top_level_statement = .{ .stmt = @enumFromInt(node.data_1), .region = store.getRegionAt(node_idx), @@ -2805,6 +3313,9 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI .diag_if_else_not_canonicalized => return CIR.Diagnostic{ .if_else_not_canonicalized = .{ .region = store.getRegionAt(node_idx), } }, + .diag_if_expr_without_else => return CIR.Diagnostic{ .if_expr_without_else = .{ + .region = store.getRegionAt(node_idx), + } }, .diag_var_across_function_boundary => return CIR.Diagnostic{ .var_across_function_boundary = .{ .region = store.getRegionAt(node_idx), } }, @@ -2849,10 +3360,25 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI .type_name = @as(base.Ident.Idx, @bitCast(node.data_2)), .region = store.getRegionAt(node_idx), } }, + .diag_type_from_missing_module => return CIR.Diagnostic{ .type_from_missing_module = .{ + .module_name = @as(base.Ident.Idx, @bitCast(node.data_1)), + .type_name = @as(base.Ident.Idx, @bitCast(node.data_2)), + .region = store.getRegionAt(node_idx), + } }, .diag_module_not_imported => return CIR.Diagnostic{ .module_not_imported = .{ .module_name = @as(base.Ident.Idx, @bitCast(node.data_1)), .region = store.getRegionAt(node_idx), } }, + .diag_nested_type_not_found => return CIR.Diagnostic{ .nested_type_not_found = .{ + .parent_name = @as(base.Ident.Idx, @bitCast(node.data_1)), + .nested_name = @as(base.Ident.Idx, @bitCast(node.data_2)), + .region = store.getRegionAt(node_idx), + } }, + .diag_nested_value_not_found => return CIR.Diagnostic{ .nested_value_not_found = .{ + .parent_name = @as(base.Ident.Idx, @bitCast(node.data_1)), + .nested_name = @as(base.Ident.Idx, @bitCast(node.data_2)), + .region = store.getRegionAt(node_idx), + } }, .diag_too_many_exports => return CIR.Diagnostic{ .too_many_exports = .{ .count = node.data_1, .region = store.getRegionAt(node_idx), @@ -2870,6 +3396,43 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI .diag_where_clause_not_allowed_in_type_decl => return CIR.Diagnostic{ .where_clause_not_allowed_in_type_decl = .{ .region = store.getRegionAt(node_idx), } }, + .diag_type_module_missing_matching_type => return CIR.Diagnostic{ .type_module_missing_matching_type = .{ + .module_name = @bitCast(node.data_1), + .region = store.getRegionAt(node_idx), + } }, + .diag_default_app_missing_main => return CIR.Diagnostic{ .default_app_missing_main = .{ + .module_name = @bitCast(node.data_1), + .region = store.getRegionAt(node_idx), + } }, + .diag_default_app_wrong_arity => return CIR.Diagnostic{ .default_app_wrong_arity = .{ + .arity = node.data_1, + .region = store.getRegionAt(node_idx), + } }, + .diag_cannot_import_default_app => return CIR.Diagnostic{ .cannot_import_default_app = .{ + .module_name = @bitCast(node.data_1), + .region = store.getRegionAt(node_idx), + } }, + .diag_execution_requires_app_or_default_app => return CIR.Diagnostic{ .execution_requires_app_or_default_app = .{ + .region = store.getRegionAt(node_idx), + } }, + .diag_type_name_case_mismatch => return CIR.Diagnostic{ .type_name_case_mismatch = .{ + .module_name = @bitCast(node.data_1), + .type_name = @bitCast(node.data_2), + .region = store.getRegionAt(node_idx), + } }, + .diag_module_header_deprecated => return CIR.Diagnostic{ .module_header_deprecated = .{ + .region = store.getRegionAt(node_idx), + } }, + .diag_redundant_expose_main_type => return CIR.Diagnostic{ .redundant_expose_main_type = .{ + .type_name = @bitCast(node.data_1), + .module_name = @bitCast(node.data_2), + .region = store.getRegionAt(node_idx), + } }, + .diag_invalid_main_type_rename_in_exposing => return CIR.Diagnostic{ .invalid_main_type_rename_in_exposing = .{ + .type_name = @bitCast(node.data_1), + .alias = @bitCast(node.data_2), + .region = store.getRegionAt(node_idx), + } }, .diag_type_alias_redeclared => return CIR.Diagnostic{ .type_alias_redeclared = .{ .name = @bitCast(node.data_1), .redeclared_region = store.getRegionAt(node_idx), @@ -2946,7 +3509,7 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI .suggested_name = @bitCast(node.data_2), .region = store.getRegionAt(node_idx), } }, - .diag_type_var_ending_in_underscore => return CIR.Diagnostic{ .type_var_ending_in_underscore = .{ + .diag_type_var_starting_with_dollar => return CIR.Diagnostic{ .type_var_starting_with_dollar = .{ .name = @bitCast(node.data_1), .suggested_name = @bitCast(node.data_2), .region = store.getRegionAt(node_idx), @@ -2962,13 +3525,13 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI } /// Computes the span of a diagnostic starting from a given index. -pub fn diagnosticSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.Diagnostic.Span { - return try store.spanFrom("scratch_diagnostics", CIR.Diagnostic.Span, start); +pub fn diagnosticSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.Diagnostic.Span { + return try store.spanFrom("diagnostics", CIR.Diagnostic.Span, start); } /// Ensure the node store has capacity for at least the requested number of /// slots. Then return the *final* index. -pub fn predictNodeIndex(store: *NodeStore, count: u32) std.mem.Allocator.Error!Node.Idx { +pub fn predictNodeIndex(store: *NodeStore, count: u32) Allocator.Error!Node.Idx { const start_idx = store.nodes.len(); try store.nodes.ensureTotalCapacity(store.gpa, start_idx + count); // Return where the LAST node will actually be placed @@ -2979,7 +3542,7 @@ pub fn predictNodeIndex(store: *NodeStore, count: u32) std.mem.Allocator.Error!N /// /// IMPORTANT: You should not use this function directly! Instead, use it's /// corresponding function in `ModuleEnv`. -pub fn addTypeVarSlot(store: *NodeStore, parent_node_idx: Node.Idx, region: base.Region) std.mem.Allocator.Error!Node.Idx { +pub fn addTypeVarSlot(store: *NodeStore, parent_node_idx: Node.Idx, region: base.Region) Allocator.Error!Node.Idx { const nid = try store.nodes.append(store.gpa, .{ .tag = .type_var_slot, .data_1 = @intFromEnum(parent_node_idx), @@ -2994,7 +3557,7 @@ pub fn addTypeVarSlot(store: *NodeStore, parent_node_idx: Node.Idx, region: base /// If it is, do nothing /// If it's not, then fill in the store with type_var_slots for all missing /// intervening nodes, *up to and including* the provided node -pub fn fillInTypeVarSlotsThru(store: *NodeStore, target_idx: Node.Idx, parent_node_idx: Node.Idx, region: Region) std.mem.Allocator.Error!void { +pub fn fillInTypeVarSlotsThru(store: *NodeStore, target_idx: Node.Idx, parent_node_idx: Node.Idx, region: Region) Allocator.Error!void { const idx = @intFromEnum(target_idx); try store.nodes.items.ensureTotalCapacity(store.gpa, idx); while (store.nodes.items.len <= idx) { @@ -3010,116 +3573,50 @@ pub fn fillInTypeVarSlotsThru(store: *NodeStore, target_idx: Node.Idx, parent_no /// Return the current top index for scratch match branches. pub fn scratchMatchBranchTop(store: *NodeStore) u32 { - return store.scratchTop("scratch_match_branches"); + return store.scratchTop("match_branches"); } /// Add a match branch index to the scratch buffer. -pub fn addScratchMatchBranch(store: *NodeStore, branch_idx: CIR.Expr.Match.Branch.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_match_branches", branch_idx); +pub fn addScratchMatchBranch(store: *NodeStore, branch_idx: CIR.Expr.Match.Branch.Idx) Allocator.Error!void { + try store.addScratch("match_branches", branch_idx); } /// Create a span from the scratch match branches starting at the given index. -pub fn matchBranchSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.Expr.Match.Branch.Span { - return try store.spanFrom("scratch_match_branches", CIR.Expr.Match.Branch.Span, start); +pub fn matchBranchSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.Expr.Match.Branch.Span { + return try store.spanFrom("match_branches", CIR.Expr.Match.Branch.Span, start); } /// Return the current top index for scratch match branch patterns. pub fn scratchMatchBranchPatternTop(store: *NodeStore) u32 { - return store.scratchTop("scratch_match_branch_patterns"); + return store.scratchTop("match_branch_patterns"); } /// Add a match branch pattern index to the scratch buffer. -pub fn addScratchMatchBranchPattern(store: *NodeStore, pattern_idx: CIR.Expr.Match.BranchPattern.Idx) std.mem.Allocator.Error!void { - try store.addScratch("scratch_match_branch_patterns", pattern_idx); +pub fn addScratchMatchBranchPattern(store: *NodeStore, pattern_idx: CIR.Expr.Match.BranchPattern.Idx) Allocator.Error!void { + try store.addScratch("match_branch_patterns", pattern_idx); } /// Create a span from the scratch match branch patterns starting at the given index. -pub fn matchBranchPatternSpanFrom(store: *NodeStore, start: u32) std.mem.Allocator.Error!CIR.Expr.Match.BranchPattern.Span { - return try store.spanFrom("scratch_match_branch_patterns", CIR.Expr.Match.BranchPattern.Span, start); -} - -/// Serialize this NodeStore to the given CompactWriter. The resulting NodeStore -/// in the writer's buffer will have offsets instead of pointers. Calling any -/// methods on it or dereferencing its internal "pointers" (which are now -/// offsets) is illegal behavior! -pub fn serialize( - self: *const NodeStore, - allocator: std.mem.Allocator, - writer: *CompactWriter, -) std.mem.Allocator.Error!*const NodeStore { - // First, write the NodeStore struct itself - const offset_self = try writer.appendAlloc(allocator, NodeStore); - - // Then serialize the sub-structures and update the struct - offset_self.* = .{ - .gpa = undefined, // Will be set when deserializing - .nodes = (try self.nodes.serialize(allocator, writer)).*, - .regions = (try self.regions.serialize(allocator, writer)).*, - .extra_data = (try self.extra_data.serialize(allocator, writer)).*, - // All scratch arrays are serialized as empty - // TODO: maybe we can put these all at the end of ModuleEnv and not bother serializing them, and just re-init on deserialization? - .scratch_statements = .{ .items = .{} }, - .scratch_exprs = .{ .items = .{} }, - .scratch_captures = .{ .items = .{} }, - .scratch_record_fields = .{ .items = .{} }, - .scratch_match_branches = .{ .items = .{} }, - .scratch_match_branch_patterns = .{ .items = .{} }, - .scratch_if_branches = .{ .items = .{} }, - .scratch_where_clauses = .{ .items = .{} }, - .scratch_patterns = .{ .items = .{} }, - .scratch_pattern_record_fields = .{ .items = .{} }, - .scratch_record_destructs = .{ .items = .{} }, - .scratch_type_annos = .{ .items = .{} }, - .scratch_anno_record_fields = .{ .items = .{} }, - .scratch_exposed_items = .{ .items = .{} }, - .scratch_defs = .{ .items = .{} }, - .scratch_diagnostics = .{ .items = .{} }, - }; - - return @constCast(offset_self); -} - -/// Add the given offset to the memory addresses of all pointers in `self`. -pub fn relocate(self: *NodeStore, offset: isize) void { - self.nodes.relocate(offset); - self.regions.relocate(offset); - self.extra_data.relocate(offset); - // Note: scratch arrays are empty after deserialization, so no need to relocate +pub fn matchBranchPatternSpanFrom(store: *NodeStore, start: u32) Allocator.Error!CIR.Expr.Match.BranchPattern.Span { + return try store.spanFrom("match_branch_patterns", CIR.Expr.Match.BranchPattern.Span, start); } /// Serialized representation of NodeStore -pub const Serialized = struct { +/// Uses extern struct to guarantee consistent field layout across optimization levels. +pub const Serialized = extern struct { + gpa: [2]u64, // Reserve enough space for 2 64-bit pointers nodes: Node.List.Serialized, regions: Region.List.Serialized, extra_data: collections.SafeList(u32).Serialized, - // Scratch arrays - not serialized, just placeholders to match NodeStore size - // TODO move these out of NodeStore so that we don't need to serialize and - // deserialize a bunch of zeros for these; it's a waste of space. - scratch_statements: std.ArrayListUnmanaged(CIR.Statement.Idx) = .{}, - scratch_exprs: std.ArrayListUnmanaged(CIR.Expr.Idx) = .{}, - scratch_record_fields: std.ArrayListUnmanaged(CIR.RecordField.Idx) = .{}, - scratch_match_branches: std.ArrayListUnmanaged(CIR.Expr.Match.Branch.Idx) = .{}, - scratch_match_branch_patterns: std.ArrayListUnmanaged(CIR.Expr.Match.BranchPattern.Idx) = .{}, - scratch_if_branches: std.ArrayListUnmanaged(CIR.Expr.IfBranch.Idx) = .{}, - scratch_where_clauses: std.ArrayListUnmanaged(CIR.WhereClause.Idx) = .{}, - scratch_patterns: std.ArrayListUnmanaged(CIR.Pattern.Idx) = .{}, - scratch_pattern_record_fields: std.ArrayListUnmanaged(CIR.PatternRecordField.Idx) = .{}, - scratch_record_destructs: std.ArrayListUnmanaged(CIR.Pattern.RecordDestruct.Idx) = .{}, - scratch_type_annos: std.ArrayListUnmanaged(CIR.TypeAnno.Idx) = .{}, - scratch_anno_record_fields: std.ArrayListUnmanaged(CIR.TypeAnno.RecordField.Idx) = .{}, - scratch_exposed_items: std.ArrayListUnmanaged(CIR.ExposedItem.Idx) = .{}, - scratch_defs: std.ArrayListUnmanaged(CIR.Def.Idx) = .{}, - scratch_diagnostics: std.ArrayListUnmanaged(CIR.Diagnostic.Idx) = .{}, - scratch_captures: std.ArrayListUnmanaged(CIR.Expr.Capture.Idx) = .{}, - gpa: std.mem.Allocator = undefined, + scratch: u64, // Reserve enough space for a 64-bit pointer /// Serialize a NodeStore into this Serialized struct, appending data to the writer pub fn serialize( self: *Serialized, store: *const NodeStore, - allocator: std.mem.Allocator, + allocator: Allocator, writer: *CompactWriter, - ) std.mem.Allocator.Error!void { + ) Allocator.Error!void { // Serialize nodes try self.nodes.serialize(&store.nodes, allocator, writer); // Serialize regions @@ -3129,35 +3626,26 @@ pub const Serialized = struct { } /// Deserialize this Serialized struct into a NodeStore - pub fn deserialize(self: *Serialized, offset: i64, gpa: std.mem.Allocator) *NodeStore { - // NodeStore.Serialized should be at least as big as NodeStore - std.debug.assert(@sizeOf(Serialized) >= @sizeOf(NodeStore)); + pub fn deserialize(self: *Serialized, offset: i64, gpa: Allocator) *NodeStore { + // Note: Serialized may be smaller than the runtime struct. + // On 32-bit platforms, deserializing nodes in-place corrupts the adjacent + // regions and extra_data fields. We must deserialize in REVERSE order (last to first) + // so that each deserialization doesn't corrupt fields that haven't been deserialized yet. - // Overwrite ourself with the deserialized version, and return our pointer after casting it to Self. + // Deserialize in reverse order: extra_data, regions, then nodes + const deserialized_extra_data = self.extra_data.deserialize(offset).*; + const deserialized_regions = self.regions.deserialize(offset).*; + const deserialized_nodes = self.nodes.deserialize(offset).*; + + // Overwrite ourself with the deserialized version, and return our pointer after casting it to NodeStore const store = @as(*NodeStore, @ptrFromInt(@intFromPtr(self))); store.* = NodeStore{ .gpa = gpa, - .nodes = self.nodes.deserialize(offset).*, - .regions = self.regions.deserialize(offset).*, - .extra_data = self.extra_data.deserialize(offset).*, - // Initialize scratch arrays as proper Scratch instances - .scratch_statements = base.Scratch(CIR.Statement.Idx){ .items = .{} }, - .scratch_exprs = base.Scratch(CIR.Expr.Idx){ .items = .{} }, - .scratch_captures = base.Scratch(CIR.Expr.Capture.Idx){ .items = .{} }, - .scratch_patterns = base.Scratch(CIR.Pattern.Idx){ .items = .{} }, - .scratch_record_fields = base.Scratch(CIR.RecordField.Idx){ .items = .{} }, - .scratch_pattern_record_fields = base.Scratch(CIR.PatternRecordField.Idx){ .items = .{} }, - .scratch_record_destructs = base.Scratch(CIR.Pattern.RecordDestruct.Idx){ .items = .{} }, - .scratch_match_branches = base.Scratch(CIR.Expr.Match.Branch.Idx){ .items = .{} }, - .scratch_match_branch_patterns = base.Scratch(CIR.Expr.Match.BranchPattern.Idx){ .items = .{} }, - .scratch_if_branches = base.Scratch(CIR.Expr.IfBranch.Idx){ .items = .{} }, - .scratch_type_annos = base.Scratch(CIR.TypeAnno.Idx){ .items = .{} }, - .scratch_anno_record_fields = base.Scratch(CIR.TypeAnno.RecordField.Idx){ .items = .{} }, - .scratch_exposed_items = base.Scratch(CIR.ExposedItem.Idx){ .items = .{} }, - .scratch_defs = base.Scratch(CIR.Def.Idx){ .items = .{} }, - .scratch_where_clauses = base.Scratch(CIR.WhereClause.Idx){ .items = .{} }, - .scratch_diagnostics = base.Scratch(CIR.Diagnostic.Idx){ .items = .{} }, + .nodes = deserialized_nodes, + .regions = deserialized_regions, + .extra_data = deserialized_extra_data, + .scratch = null, // A deserialized NodeStore is read-only, so it has no need for scratch memory! }; return store; @@ -3192,7 +3680,7 @@ test "NodeStore empty CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @intCast(file_size)); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @intCast(file_size)); defer gpa.free(buffer); _ = try file.read(buffer); @@ -3222,7 +3710,7 @@ test "NodeStore basic CompactWriter roundtrip" { .data_2 = 0, .data_3 = 0, }; - _ = try original.nodes.append(gpa, node1); + const node1_idx = try original.nodes.append(gpa, node1); // Add integer value to extra_data (i128 as 4 u32s) const value: i128 = 42; @@ -3237,7 +3725,7 @@ test "NodeStore basic CompactWriter roundtrip" { .start = .{ .offset = 0 }, .end = .{ .offset = 5 }, }; - _ = try original.regions.append(gpa, region); + const region1_idx = try original.regions.append(gpa, region); // Create a temp file var tmp_dir = testing.tmpDir(.{}); @@ -3259,7 +3747,7 @@ test "NodeStore basic CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @intCast(file_size)); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @intCast(file_size)); defer gpa.free(buffer); _ = try file.read(buffer); @@ -3270,7 +3758,7 @@ test "NodeStore basic CompactWriter roundtrip" { // Verify nodes try testing.expectEqual(@as(usize, 1), deserialized.nodes.len()); - const retrieved_node = deserialized.nodes.get(@enumFromInt(0)); + const retrieved_node = deserialized.nodes.get(node1_idx); try testing.expectEqual(Node.Tag.expr_int, retrieved_node.tag); try testing.expectEqual(@as(u32, 0), retrieved_node.data_1); @@ -3283,7 +3771,7 @@ test "NodeStore basic CompactWriter roundtrip" { // Verify regions try testing.expectEqual(@as(usize, 1), deserialized.regions.len()); - const retrieved_region = deserialized.regions.get(@enumFromInt(0)); + const retrieved_region = deserialized.regions.get(region1_idx); try testing.expectEqual(region.start.offset, retrieved_region.start.offset); try testing.expectEqual(region.end.offset, retrieved_region.end.offset); } @@ -3303,16 +3791,16 @@ test "NodeStore multiple nodes CompactWriter roundtrip" { .data_2 = 0, .data_3 = 0, }; - _ = try original.nodes.append(gpa, var_node); + const var_node_idx = try original.nodes.append(gpa, var_node); // Add expression list node const list_node = Node{ .tag = .expr_list, .data_1 = 10, // elems start .data_2 = 3, // elems len - .data_3 = 2, // elem_var + .data_3 = 0, }; - _ = try original.nodes.append(gpa, list_node); + const list_node_idx = try original.nodes.append(gpa, list_node); // Add float node with extra data const float_node = Node{ @@ -3321,7 +3809,7 @@ test "NodeStore multiple nodes CompactWriter roundtrip" { .data_2 = 0, .data_3 = 0, }; - _ = try original.nodes.append(gpa, float_node); + const float_node_idx = try original.nodes.append(gpa, float_node); // Add float value to extra_data const float_value: f64 = 3.14159; @@ -3332,14 +3820,12 @@ test "NodeStore multiple nodes CompactWriter roundtrip" { } // Add regions for each node - const regions = [_]Region{ - .{ .start = .{ .offset = 0 }, .end = .{ .offset = 5 } }, - .{ .start = .{ .offset = 10 }, .end = .{ .offset = 20 } }, - .{ .start = .{ .offset = 25 }, .end = .{ .offset = 32 } }, - }; - for (regions) |region| { - _ = try original.regions.append(gpa, region); - } + const region1 = Region{ .start = .{ .offset = 0 }, .end = .{ .offset = 5 } }; + const region2 = Region{ .start = .{ .offset = 10 }, .end = .{ .offset = 20 } }; + const region3 = Region{ .start = .{ .offset = 25 }, .end = .{ .offset = 32 } }; + const region1_idx = try original.regions.append(gpa, region1); + const region2_idx = try original.regions.append(gpa, region2); + const region3_idx = try original.regions.append(gpa, region3); // Create a temp file var tmp_dir = testing.tmpDir(.{}); @@ -3361,7 +3847,7 @@ test "NodeStore multiple nodes CompactWriter roundtrip" { // Read back try file.seekTo(0); const file_size = try file.getEndPos(); - const buffer = try gpa.alignedAlloc(u8, 16, @intCast(file_size)); + const buffer = try gpa.alignedAlloc(u8, std.mem.Alignment.@"16", @intCast(file_size)); defer gpa.free(buffer); _ = try file.read(buffer); @@ -3373,36 +3859,37 @@ test "NodeStore multiple nodes CompactWriter roundtrip" { // Verify nodes try testing.expectEqual(@as(usize, 3), deserialized.nodes.len()); - // Verify var node - const retrieved_var = deserialized.nodes.get(@enumFromInt(0)); + // Verify var node using captured index + const retrieved_var = deserialized.nodes.get(var_node_idx); try testing.expectEqual(Node.Tag.expr_var, retrieved_var.tag); try testing.expectEqual(@as(u32, 5), retrieved_var.data_1); - // Verify list node - const retrieved_list = deserialized.nodes.get(@enumFromInt(1)); + // Verify list node using captured index + const retrieved_list = deserialized.nodes.get(list_node_idx); try testing.expectEqual(Node.Tag.expr_list, retrieved_list.tag); try testing.expectEqual(@as(u32, 10), retrieved_list.data_1); try testing.expectEqual(@as(u32, 3), retrieved_list.data_2); - try testing.expectEqual(@as(u32, 2), retrieved_list.data_3); - // Verify float node and extra data - const retrieved_float = deserialized.nodes.get(@enumFromInt(2)); + // Verify float node and extra data using captured index + const retrieved_float = deserialized.nodes.get(float_node_idx); try testing.expectEqual(Node.Tag.expr_frac_f64, retrieved_float.tag); const retrieved_float_u32s = deserialized.extra_data.items.items[0..2]; const retrieved_float_u64: u64 = @bitCast(retrieved_float_u32s.*); const retrieved_float_value: f64 = @bitCast(retrieved_float_u64); try testing.expectApproxEqAbs(float_value, retrieved_float_value, 0.0001); - // Verify regions + // Verify regions using captured indices try testing.expectEqual(@as(usize, 3), deserialized.regions.len()); - for (regions, 0..) |expected_region, i| { - const retrieved_region = deserialized.regions.get(@enumFromInt(i)); - try testing.expectEqual(expected_region.start.offset, retrieved_region.start.offset); - try testing.expectEqual(expected_region.end.offset, retrieved_region.end.offset); - } + const retrieved_region1 = deserialized.regions.get(region1_idx); + try testing.expectEqual(region1.start.offset, retrieved_region1.start.offset); + try testing.expectEqual(region1.end.offset, retrieved_region1.end.offset); + const retrieved_region2 = deserialized.regions.get(region2_idx); + try testing.expectEqual(region2.start.offset, retrieved_region2.start.offset); + try testing.expectEqual(region2.end.offset, retrieved_region2.end.offset); + const retrieved_region3 = deserialized.regions.get(region3_idx); + try testing.expectEqual(region3.start.offset, retrieved_region3.start.offset); + try testing.expectEqual(region3.end.offset, retrieved_region3.end.offset); - // Verify all scratch arrays are empty - try testing.expectEqual(@as(usize, 0), deserialized.scratch_statements.items.items.len); - try testing.expectEqual(@as(usize, 0), deserialized.scratch_exprs.items.items.len); - try testing.expectEqual(@as(usize, 0), deserialized.scratch_patterns.items.items.len); + // Verify scratch is null (deserialized NodeStores don't allocate scratch) + try testing.expect(deserialized.scratch == null); } diff --git a/src/canonicalize/Pattern.zig b/src/canonicalize/Pattern.zig index 595d9fc14a..7efe075a13 100644 --- a/src/canonicalize/Pattern.zig +++ b/src/canonicalize/Pattern.zig @@ -66,7 +66,7 @@ pub const Pattern = union(enum) { /// Used for pattern matching nominal types. /// /// ```roc - /// Result.Ok("success") # Tags + /// Try.Ok("success") # Tags /// Config.{ optimize : Bool} # Records /// Point.(1.0, 2.0) # Tuples /// Point.(1.0) # Values @@ -80,7 +80,7 @@ pub const Pattern = union(enum) { /// Used for pattern matching nominal types. /// /// ```roc - /// MyModule.Result.Ok("success") # Tags + /// MyModule.Try.Ok("success") # Tags /// MyModule.Config.{ optimize : Bool} # Records /// MyModule.Point.(1.0, 2.0) # Tuples /// MyModule.Point.(1.0) # Values @@ -101,8 +101,6 @@ pub const Pattern = union(enum) { /// } /// ``` record_destructure: struct { - whole_var: TypeVar, - ext_var: TypeVar, destructs: RecordDestruct.Span, }, /// Pattern that destructures a list, with optional rest pattern. @@ -118,8 +116,6 @@ pub const Pattern = union(enum) { /// } /// ``` list: struct { - list_var: TypeVar, - elem_var: TypeVar, patterns: Pattern.Span, // All non-rest patterns rest_info: ?struct { index: u32, // Where the rest appears (split point) @@ -148,8 +144,9 @@ pub const Pattern = union(enum) { /// n => "many" /// } /// ``` - int_literal: struct { + num_literal: struct { value: CIR.IntValue, + kind: CIR.NumKind, }, /// Pattern that matches a small decimal literal (represented as rational number). /// This is Roc's preferred approach for exact decimal matching, avoiding @@ -163,8 +160,8 @@ pub const Pattern = union(enum) { /// } /// ``` small_dec_literal: struct { - numerator: i16, - denominator_power_of_ten: u8, + value: CIR.SmallDecValue, + has_suffix: bool, }, /// Pattern that matches a high-precision decimal literal. /// Used for exact decimal matching with arbitrary precision. @@ -177,6 +174,7 @@ pub const Pattern = union(enum) { /// ``` dec_literal: struct { value: RocDec, + has_suffix: bool, }, /// Pattern that matches a specific f32 literal value exactly. /// Used for exact matching in pattern expressions. @@ -233,7 +231,7 @@ pub const Pattern = union(enum) { }, pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: base.DataSpan }; + pub const Span = extern struct { span: base.DataSpan }; /// Represents the destructuring of a single field within a record pattern. /// Each record destructure specifies how to extract a field from a record. @@ -249,7 +247,7 @@ pub const Pattern = union(enum) { kind: Kind, pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: base.DataSpan }; + pub const Span = extern struct { span: base.DataSpan }; /// The kind of record field destructuring pattern. pub const Kind = union(enum) { @@ -354,15 +352,19 @@ pub const Pattern = union(enum) { try tree.pushStaticAtom("p-nominal-external"); try ir.appendRegionInfoToSExprTree(tree, pattern_idx); - // Add module index - var buf: [32]u8 = undefined; - const module_idx_str = std.fmt.bufPrint(&buf, "{}", .{@intFromEnum(n.module_idx)}) catch unreachable; - try tree.pushStringPair("module-idx", module_idx_str); - - // Add target node index - var buf2: [32]u8 = undefined; - const target_idx_str = std.fmt.bufPrint(&buf2, "{}", .{n.target_node_idx}) catch unreachable; - try tree.pushStringPair("target-node-idx", target_idx_str); + const module_idx_int = @intFromEnum(n.module_idx); + std.debug.assert(module_idx_int < ir.imports.imports.items.items.len); + const string_lit_idx = ir.imports.imports.items.items[module_idx_int]; + const module_name = ir.common.strings.get(string_lit_idx); + // Special case: Builtin module is an implementation detail, print as (builtin) + if (std.mem.eql(u8, module_name, "Builtin")) { + const field_begin = tree.beginNode(); + try tree.pushStaticAtom("builtin"); + const field_attrs = tree.beginNode(); + try tree.endNode(field_begin, field_attrs); + } else { + try tree.pushStringPair("external-module", module_name); + } const attrs = tree.beginNode(); try ir.store.getPattern(n.backing_pattern).pushToSExprTree(ir, tree, n.backing_pattern); @@ -435,14 +437,13 @@ pub const Pattern = union(enum) { try tree.endNode(begin, attrs); }, - .int_literal => |p| { + .num_literal => |p| { const begin = tree.beginNode(); - try tree.pushStaticAtom("p-int"); + try tree.pushStaticAtom("p-num"); try ir.appendRegionInfoToSExprTree(tree, pattern_idx); - const value_i128: i128 = @bitCast(p.value.bytes); var value_buf: [40]u8 = undefined; - const value_str = std.fmt.bufPrint(&value_buf, "{}", .{value_i128}) catch "fmt_error"; + const value_str = p.value.bufPrint(&value_buf) catch unreachable; try tree.pushStringPair("value", value_str); const attrs = tree.beginNode(); @@ -468,7 +469,7 @@ pub const Pattern = union(enum) { try ir.appendRegionInfoToSExprTree(tree, pattern_idx); var value_buf: [40]u8 = undefined; - const value_str = std.fmt.bufPrint(&value_buf, "{}", .{p.value}) catch "fmt_error"; + const value_str = std.fmt.bufPrint(&value_buf, "{e}", .{p.value}) catch "fmt_error"; try tree.pushStringPair("value", value_str); const attrs = tree.beginNode(); @@ -480,7 +481,7 @@ pub const Pattern = union(enum) { try ir.appendRegionInfoToSExprTree(tree, pattern_idx); var value_buf: [40]u8 = undefined; - const value_str = std.fmt.bufPrint(&value_buf, "{}", .{p.value}) catch "fmt_error"; + const value_str = std.fmt.bufPrint(&value_buf, "{e}", .{p.value}) catch "fmt_error"; try tree.pushStringPair("value", value_str); const attrs = tree.beginNode(); diff --git a/src/canonicalize/RocEmitter.zig b/src/canonicalize/RocEmitter.zig new file mode 100644 index 0000000000..ede7b76c6e --- /dev/null +++ b/src/canonicalize/RocEmitter.zig @@ -0,0 +1,628 @@ +//! Roc Code Emitter +//! +//! Converts the Canonical IR (CIR) to valid Roc source code. +//! This is primarily used for testing the monomorphization pipeline - +//! we can emit monomorphic Roc code and verify it produces the same results +//! as the original polymorphic code. +//! +//! The emitter walks the CIR expression tree and writes corresponding Roc syntax. + +const std = @import("std"); +const types = @import("types"); +const base = @import("base"); + +const ModuleEnv = @import("ModuleEnv.zig"); +const CIR = @import("CIR.zig"); +const PatternMod = @import("Pattern.zig"); +const Expr = CIR.Expr; +const Pattern = PatternMod.Pattern; +const TypeVar = types.Var; + +const Self = @This(); + +/// The allocator used for intermediate allocations +allocator: std.mem.Allocator, + +/// The module environment containing the CIR +module_env: *const ModuleEnv, + +/// The output buffer where Roc code is written +output: std.ArrayList(u8), + +/// Current indentation level +indent_level: u32, + +/// Initialize a new Emitter +pub fn init(allocator: std.mem.Allocator, module_env: *const ModuleEnv) Self { + return .{ + .allocator = allocator, + .module_env = module_env, + .output = std.ArrayList(u8).empty, + .indent_level = 0, + }; +} + +/// Free resources used by the emitter +pub fn deinit(self: *Self) void { + self.output.deinit(self.allocator); +} + +/// Get the emitted Roc source code +pub fn getOutput(self: *const Self) []const u8 { + return self.output.items; +} + +/// Reset the emitter for reuse +pub fn reset(self: *Self) void { + self.output.clearRetainingCapacity(); + self.indent_level = 0; +} + +/// Emit an expression as Roc source code +pub fn emitExpr(self: *Self, expr_idx: Expr.Idx) !void { + const expr = self.module_env.store.getExpr(expr_idx); + try self.emitExprValue(expr); +} + +/// Emit a pattern as Roc source code +pub fn emitPattern(self: *Self, pattern_idx: CIR.Pattern.Idx) !void { + const pattern = self.module_env.store.getPattern(pattern_idx); + try self.emitPatternValue(pattern); +} + +const EmitError = std.mem.Allocator.Error || std.fmt.BufPrintError; + +fn emitExprValue(self: *Self, expr: Expr) EmitError!void { + switch (expr) { + .e_num => |num| { + try self.emitIntValue(num.value); + }, + .e_frac_f32 => |frac| { + try self.writer().print("{d}f32", .{frac.value}); + }, + .e_frac_f64 => |frac| { + try self.writer().print("{d}f64", .{frac.value}); + }, + .e_dec => |dec| { + // Dec is stored scaled by 10^18, need to emit as decimal + const value = dec.value.num; + const scale: i128 = 1_000_000_000_000_000_000; + const whole = @divTrunc(value, scale); + const frac_part = @mod(@abs(value), @as(u128, @intCast(scale))); + if (frac_part == 0) { + try self.writer().print("{d}", .{whole}); + } else { + try self.writer().print("{d}.{d:0>18}", .{ whole, frac_part }); + } + }, + .e_dec_small => |small| { + const numerator = small.value.numerator; + const power = small.value.denominator_power_of_ten; + if (power == 0) { + try self.writer().print("{d}", .{numerator}); + } else { + // Convert to decimal string + var divisor: i32 = 1; + for (0..power) |_| { + divisor *= 10; + } + const whole = @divTrunc(numerator, @as(i16, @intCast(divisor))); + const frac_part = @mod(@abs(numerator), @as(u16, @intCast(divisor))); + try self.writer().print("{d}.{d}", .{ whole, frac_part }); + } + }, + .e_str_segment => |seg| { + const text = self.module_env.common.getString(seg.literal); + try self.writer().print("\"{s}\"", .{text}); + }, + .e_str => |str| { + // Multi-segment string + const segments = self.module_env.store.sliceExpr(str.span); + for (segments) |seg_idx| { + try self.emitExpr(seg_idx); + } + }, + .e_lookup_local => |lookup| { + const pattern = self.module_env.store.getPattern(lookup.pattern_idx); + try self.emitPatternValue(pattern); + }, + .e_lookup_external => { + // For external lookups, emit the qualified name + try self.write(""); + }, + .e_list => |list| { + try self.write("["); + const elems = self.module_env.store.sliceExpr(list.elems); + for (elems, 0..) |elem_idx, i| { + if (i > 0) try self.write(", "); + try self.emitExpr(elem_idx); + } + try self.write("]"); + }, + .e_empty_list => { + try self.write("[]"); + }, + .e_tuple => |tuple| { + try self.write("("); + const elems = self.module_env.store.sliceExpr(tuple.elems); + for (elems, 0..) |elem_idx, i| { + if (i > 0) try self.write(", "); + try self.emitExpr(elem_idx); + } + try self.write(")"); + }, + .e_if => |if_expr| { + const branch_indices = self.module_env.store.sliceIfBranches(if_expr.branches); + for (branch_indices, 0..) |branch_idx, i| { + const branch = self.module_env.store.getIfBranch(branch_idx); + if (i > 0) { + try self.write(" else if "); + } else { + try self.write("if "); + } + try self.emitExpr(branch.cond); + try self.write(" "); + try self.emitExpr(branch.body); + } + try self.write(" else "); + try self.emitExpr(if_expr.final_else); + }, + .e_call => |call| { + try self.emitExpr(call.func); + try self.write("("); + const args = self.module_env.store.sliceExpr(call.args); + for (args, 0..) |arg_idx, i| { + if (i > 0) try self.write(", "); + try self.emitExpr(arg_idx); + } + try self.write(")"); + }, + .e_record => |record| { + try self.write("{"); + const field_indices = self.module_env.store.sliceRecordFields(record.fields); + for (field_indices, 0..) |field_idx, i| { + const field = self.module_env.store.getRecordField(field_idx); + if (i > 0) try self.write(", "); + const name = self.module_env.getIdent(field.name); + try self.writer().print("{s}: ", .{name}); + try self.emitExpr(field.value); + } + if (record.ext) |ext_idx| { + if (field_indices.len > 0) try self.write(", "); + try self.write(".."); + try self.emitExpr(ext_idx); + } + try self.write("}"); + }, + .e_empty_record => { + try self.write("{}"); + }, + .e_block => |block| { + try self.write("{\n"); + self.indent_level += 1; + + // Emit statements + const stmts = self.module_env.store.sliceStatements(block.stmts); + for (stmts) |stmt_idx| { + try self.emitIndent(); + try self.emitStatement(stmt_idx); + try self.write("\n"); + } + + // Emit final expression + try self.emitIndent(); + try self.emitExpr(block.final_expr); + try self.write("\n"); + + self.indent_level -= 1; + try self.emitIndent(); + try self.write("}"); + }, + .e_tag => |tag| { + const name = self.module_env.getIdent(tag.name); + try self.write(name); + const args = self.module_env.store.sliceExpr(tag.args); + if (args.len > 0) { + try self.write("("); + for (args, 0..) |arg_idx, i| { + if (i > 0) try self.write(", "); + try self.emitExpr(arg_idx); + } + try self.write(")"); + } + }, + .e_zero_argument_tag => |tag| { + const name = self.module_env.getIdent(tag.name); + try self.write(name); + }, + .e_closure => |closure| { + // Emit the underlying lambda + try self.emitExpr(closure.lambda_idx); + }, + .e_lambda => |lambda| { + try self.write("|"); + const args = self.module_env.store.slicePatterns(lambda.args); + for (args, 0..) |arg_idx, i| { + if (i > 0) try self.write(", "); + try self.emitPattern(arg_idx); + } + try self.write("| "); + try self.emitExpr(lambda.body); + }, + .e_binop => |binop| { + try self.write("("); + try self.emitExpr(binop.lhs); + try self.write(" "); + try self.write(binopToStr(binop.op)); + try self.write(" "); + try self.emitExpr(binop.rhs); + try self.write(")"); + }, + .e_unary_minus => |unary| { + try self.write("-"); + try self.emitExpr(unary.expr); + }, + .e_unary_not => |unary| { + try self.write("!"); + try self.emitExpr(unary.expr); + }, + .e_dot_access => |dot| { + try self.emitExpr(dot.receiver); + try self.write("."); + const field_name = self.module_env.getIdent(dot.field_name); + try self.write(field_name); + if (dot.args) |args_span| { + try self.write("("); + const args = self.module_env.store.sliceExpr(args_span); + for (args, 0..) |arg_idx, i| { + if (i > 0) try self.write(", "); + try self.emitExpr(arg_idx); + } + try self.write(")"); + } + }, + .e_runtime_error => { + try self.write(""); + }, + .e_crash => |crash| { + const msg = self.module_env.common.getString(crash.msg); + try self.writer().print("crash \"{s}\"", .{msg}); + }, + .e_dbg => |dbg| { + try self.write("dbg "); + try self.emitExpr(dbg.expr); + }, + .e_expect => |expect| { + try self.write("expect "); + try self.emitExpr(expect.body); + }, + .e_ellipsis => { + try self.write("..."); + }, + .e_anno_only => { + try self.write(""); + }, + .e_return => |ret| { + try self.write("return "); + try self.emitExpr(ret.expr); + }, + .e_match => |match| { + try self.write("match "); + try self.emitExpr(match.cond); + try self.write(" {\n"); + self.indent_level += 1; + const branch_indices = self.module_env.store.sliceMatchBranches(match.branches); + for (branch_indices) |branch_idx| { + const branch = self.module_env.store.getMatchBranch(branch_idx); + try self.emitIndent(); + // Emit patterns + const pattern_indices = self.module_env.store.sliceMatchBranchPatterns(branch.patterns); + for (pattern_indices, 0..) |pat_entry_idx, i| { + const pat_entry = self.module_env.store.getMatchBranchPattern(pat_entry_idx); + if (i > 0) try self.write(" | "); + try self.emitPattern(pat_entry.pattern); + } + // Emit guard if present + if (branch.guard) |guard_idx| { + try self.write(" if "); + try self.emitExpr(guard_idx); + } + try self.write(" => "); + try self.emitExpr(branch.value); + try self.write(",\n"); + } + self.indent_level -= 1; + try self.emitIndent(); + try self.write("}"); + }, + .e_nominal => |nominal| { + // Emit the backing expression for now + try self.emitExpr(nominal.backing_expr); + }, + .e_nominal_external => |nominal| { + try self.emitExpr(nominal.backing_expr); + }, + .e_lookup_required => { + try self.write(""); + }, + .e_type_var_dispatch => { + try self.write(""); + }, + .e_for => |for_expr| { + try self.write("for "); + try self.emitPattern(for_expr.patt); + try self.write(" in "); + try self.emitExpr(for_expr.expr); + try self.write(" "); + try self.emitExpr(for_expr.body); + }, + .e_hosted_lambda => { + try self.write(""); + }, + .e_low_level_lambda => { + try self.write(""); + }, + } +} + +fn emitPatternValue(self: *Self, pattern: Pattern) EmitError!void { + switch (pattern) { + .assign => |ident| { + const name = self.module_env.getIdent(ident.ident); + try self.write(name); + }, + .underscore => { + try self.write("_"); + }, + .num_literal => |num| { + try self.emitIntValue(num.value); + }, + .str_literal => |str| { + const text = self.module_env.common.getString(str.literal); + try self.writer().print("\"{s}\"", .{text}); + }, + .applied_tag => |tag| { + const name = self.module_env.getIdent(tag.name); + try self.write(name); + const args = self.module_env.store.slicePatterns(tag.args); + if (args.len > 0) { + try self.write("("); + for (args, 0..) |arg_idx, i| { + if (i > 0) try self.write(", "); + try self.emitPattern(arg_idx); + } + try self.write(")"); + } + }, + .record_destructure => |record| { + try self.write("{"); + const destruct_indices = self.module_env.store.sliceRecordDestructs(record.destructs); + for (destruct_indices, 0..) |destruct_idx, i| { + const destruct = self.module_env.store.getRecordDestruct(destruct_idx); + if (i > 0) try self.write(", "); + const name = self.module_env.getIdent(destruct.label); + try self.write(name); + switch (destruct.kind) { + .Required => |pat_idx| { + // Check if the pattern is just an assign with same name + const inner_pat = self.module_env.store.getPattern(pat_idx); + switch (inner_pat) { + .assign => |inner_assign| { + const inner_name = self.module_env.getIdent(inner_assign.ident); + if (!std.mem.eql(u8, name, inner_name)) { + try self.write(": "); + try self.emitPattern(pat_idx); + } + }, + else => { + try self.write(": "); + try self.emitPattern(pat_idx); + }, + } + }, + .SubPattern => |pat_idx| { + try self.write(": "); + try self.emitPattern(pat_idx); + }, + } + } + try self.write("}"); + }, + .tuple => |t| { + try self.write("("); + const elems = self.module_env.store.slicePatterns(t.patterns); + for (elems, 0..) |elem_idx, i| { + if (i > 0) try self.write(", "); + try self.emitPattern(elem_idx); + } + try self.write(")"); + }, + .list => |l| { + try self.write("["); + const elems = self.module_env.store.slicePatterns(l.patterns); + for (elems, 0..) |elem_idx, i| { + if (i > 0) try self.write(", "); + try self.emitPattern(elem_idx); + } + if (l.rest_info) |rest| { + if (elems.len > 0) try self.write(", "); + try self.write(".."); + if (rest.pattern) |rest_pat| { + try self.emitPattern(rest_pat); + } + } + try self.write("]"); + }, + .as => |as_pat| { + try self.emitPattern(as_pat.pattern); + try self.write(" as "); + const name = self.module_env.getIdent(as_pat.ident); + try self.write(name); + }, + .runtime_error => { + try self.write(""); + }, + .nominal => |nom| { + try self.emitPattern(nom.backing_pattern); + }, + .nominal_external => |nom| { + try self.emitPattern(nom.backing_pattern); + }, + .small_dec_literal => |dec| { + const numerator = dec.value.numerator; + const power = dec.value.denominator_power_of_ten; + if (power == 0) { + try self.writer().print("{d}", .{numerator}); + } else { + var divisor: i32 = 1; + for (0..power) |_| { + divisor *= 10; + } + const whole = @divTrunc(numerator, @as(i16, @intCast(divisor))); + const frac_part = @mod(@abs(numerator), @as(u16, @intCast(divisor))); + try self.writer().print("{d}.{d}", .{ whole, frac_part }); + } + }, + .dec_literal => |dec| { + const value = dec.value.num; + const scale: i128 = 1_000_000_000_000_000_000; + const whole = @divTrunc(value, scale); + const frac_part = @mod(@abs(value), @as(u128, @intCast(scale))); + if (frac_part == 0) { + try self.writer().print("{d}", .{whole}); + } else { + try self.writer().print("{d}.{d:0>18}", .{ whole, frac_part }); + } + }, + .frac_f32_literal => |frac| { + try self.writer().print("{d}f32", .{frac.value}); + }, + .frac_f64_literal => |frac| { + try self.writer().print("{d}f64", .{frac.value}); + }, + } +} + +fn emitStatement(self: *Self, stmt_idx: CIR.Statement.Idx) EmitError!void { + const stmt = self.module_env.store.getStatement(stmt_idx); + switch (stmt) { + .s_decl => |decl| { + try self.emitPattern(decl.pattern); + try self.write(" = "); + try self.emitExpr(decl.expr); + }, + .s_decl_gen => |decl| { + try self.emitPattern(decl.pattern); + try self.write(" = "); + try self.emitExpr(decl.expr); + }, + .s_type_anno, .s_type_var_alias, .s_alias_decl, .s_nominal_decl => { + // Type declarations are not emitted for now + }, + else => {}, + } +} + +fn emitIntValue(self: *Self, value: CIR.IntValue) !void { + var buf: [64]u8 = undefined; + const str = try value.bufPrint(&buf); + try self.write(str); +} + +fn emitIndent(self: *Self) !void { + for (0..self.indent_level) |_| { + try self.write(" "); + } +} + +fn write(self: *Self, str: []const u8) !void { + try self.output.appendSlice(self.allocator, str); +} + +fn writer(self: *Self) std.ArrayList(u8).Writer { + return self.output.writer(self.allocator); +} + +fn binopToStr(op: Expr.Binop.Op) []const u8 { + return switch (op) { + .add => "+", + .sub => "-", + .mul => "*", + .div => "/", + .div_trunc => "//", + .rem => "%", + .lt => "<", + .gt => ">", + .le => "<=", + .ge => ">=", + .eq => "==", + .ne => "!=", + .@"and" => "and", + .@"or" => "or", + }; +} + +// Tests +test "emit simple integer" { + const allocator = std.testing.allocator; + + // Create a minimal test environment + const module_env = try allocator.create(ModuleEnv); + module_env.* = try ModuleEnv.init(allocator, "42"); + defer { + module_env.deinit(); + allocator.destroy(module_env); + } + + var emitter = Self.init(allocator, module_env); + defer emitter.deinit(); + + // Create a simple integer expression + const int_value = CIR.IntValue{ + .bytes = @bitCast(@as(i128, 42)), + .kind = .i128, + }; + const expr_idx = try module_env.store.addExpr(.{ + .e_num = .{ .value = int_value, .kind = .i64 }, + }, base.Region.zero()); + + try emitter.emitExpr(expr_idx); + try std.testing.expectEqualStrings("42", emitter.getOutput()); +} + +test "emit lambda expression" { + const allocator = std.testing.allocator; + + const module_env = try allocator.create(ModuleEnv); + module_env.* = try ModuleEnv.init(allocator, "|x| x"); + defer { + module_env.deinit(); + allocator.destroy(module_env); + } + + var emitter = Self.init(allocator, module_env); + defer emitter.deinit(); + + // Create pattern for 'x' + const x_ident = try module_env.insertIdent(base.Ident.for_text("x")); + const x_pattern_idx = try module_env.store.addPattern(.{ + .assign = .{ .ident = x_ident }, + }, base.Region.zero()); + + // Create lookup expression for body + const body_idx = try module_env.store.addExpr(.{ + .e_lookup_local = .{ .pattern_idx = x_pattern_idx }, + }, base.Region.zero()); + + // Create lambda expression using scratch system + const start = module_env.store.scratchPatternTop(); + try module_env.store.addScratchPattern(x_pattern_idx); + const args_span = try module_env.store.patternSpanFrom(start); + + const lambda_idx = try module_env.store.addExpr(.{ + .e_lambda = .{ .args = args_span, .body = body_idx }, + }, base.Region.zero()); + + try emitter.emitExpr(lambda_idx); + try std.testing.expectEqualStrings("|x| x", emitter.getOutput()); +} diff --git a/src/canonicalize/Scope.zig b/src/canonicalize/Scope.zig index d0ea372c71..9c6f4e590c 100644 --- a/src/canonicalize/Scope.zig +++ b/src/canonicalize/Scope.zig @@ -7,35 +7,89 @@ const collections = @import("collections"); const CIR = @import("CIR.zig"); const Ident = base.Ident; +const Region = base.Region; const Scope = @This(); +/// Represents a type binding for a type imported from an external module. +/// Contains all necessary information to resolve the type from the imported module. +pub const ExternalTypeBinding = struct { + module_ident: Ident.Idx, + original_ident: Ident.Idx, + target_node_idx: ?u16, + import_idx: ?CIR.Import.Idx, + origin_region: Region, + /// True if the module was attempted to be imported but was not found. + /// This allows us to emit a more specific diagnostic when the type is used. + module_not_found: bool, +}; + +/// A unified type binding that can represent either a locally declared type or an externally imported type. +/// This is the single source of truth for all type resolution in a scope. +pub const TypeBinding = union(enum) { + local_nominal: CIR.Statement.Idx, + local_alias: CIR.Statement.Idx, + associated_nominal: CIR.Statement.Idx, + external_nominal: ExternalTypeBinding, +}; + +/// Information about a forward reference (referenced before defined) +pub const ForwardReference = struct { + /// The pattern index created for this forward reference + pattern_idx: CIR.Pattern.Idx, + /// Regions where this identifier was referenced (for error reporting) + reference_regions: std.ArrayList(Region), +}; + +/// Information about a type variable alias binding (for static dispatch on type vars) +/// Example: `Thing : thing` creates an alias allowing `Thing.method(arg)` to dispatch +/// based on what `thing` resolves to at runtime. +pub const TypeVarAliasBinding = struct { + /// The name of the type variable being aliased (e.g., "thing") + type_var_name: Ident.Idx, + /// The type annotation index for the type variable + type_var_anno: CIR.TypeAnno.Idx, + /// The statement index for the s_type_var_alias statement + statement_idx: CIR.Statement.Idx, +}; + /// Maps an Ident to a Pattern in the Can IR idents: std.AutoHashMapUnmanaged(Ident.Idx, CIR.Pattern.Idx), aliases: std.AutoHashMapUnmanaged(Ident.Idx, CIR.Pattern.Idx), -/// Maps type names to their type declaration statements -type_decls: std.AutoHashMapUnmanaged(Ident.Idx, CIR.Statement.Idx), +/// Forward references: identifiers that have been referenced but not yet defined +forward_references: std.AutoHashMapUnmanaged(Ident.Idx, ForwardReference), +/// Canonical bindings for type names (local, auto-imported, and imported types) +type_bindings: std.AutoHashMapUnmanaged(Ident.Idx, TypeBinding), /// Maps type variables to their type annotation indices type_vars: std.AutoHashMapUnmanaged(Ident.Idx, CIR.TypeAnno.Idx), -/// Maps module alias names to their full module names -module_aliases: std.AutoHashMapUnmanaged(Ident.Idx, Ident.Idx), +/// Maps uppercase alias names to type variable aliases (for static dispatch on type vars) +/// The key is the alias name (e.g., "Thing"), the value contains the type var info +type_var_aliases: std.AutoHashMapUnmanaged(Ident.Idx, TypeVarAliasBinding), +/// Maps module alias names to their full module info (name + whether package-qualified) +module_aliases: std.AutoHashMapUnmanaged(Ident.Idx, ModuleAliasInfo), /// Maps exposed item names to their source modules and original names (for import resolution) exposed_items: std.AutoHashMapUnmanaged(Ident.Idx, ExposedItemInfo), /// Maps module names to their Import.Idx for modules imported in this scope imported_modules: std.StringHashMapUnmanaged(CIR.Import.Idx), is_function_boundary: bool, +/// The type name associated with this scope (if this scope is for an associated block) +/// null for regular scopes, set for scopes created by processAssociatedBlock +associated_type_name: ?Ident.Idx, /// Initialize the scope pub fn init(is_function_boundary: bool) Scope { return Scope{ .idents = std.AutoHashMapUnmanaged(Ident.Idx, CIR.Pattern.Idx){}, .aliases = std.AutoHashMapUnmanaged(Ident.Idx, CIR.Pattern.Idx){}, - .type_decls = std.AutoHashMapUnmanaged(Ident.Idx, CIR.Statement.Idx){}, + .forward_references = std.AutoHashMapUnmanaged(Ident.Idx, ForwardReference){}, + .type_bindings = std.AutoHashMapUnmanaged(Ident.Idx, TypeBinding){}, .type_vars = std.AutoHashMapUnmanaged(Ident.Idx, CIR.TypeAnno.Idx){}, - .module_aliases = std.AutoHashMapUnmanaged(Ident.Idx, Ident.Idx){}, + .type_var_aliases = std.AutoHashMapUnmanaged(Ident.Idx, TypeVarAliasBinding){}, + .module_aliases = std.AutoHashMapUnmanaged(Ident.Idx, ModuleAliasInfo){}, .exposed_items = std.AutoHashMapUnmanaged(Ident.Idx, ExposedItemInfo){}, .imported_modules = std.StringHashMapUnmanaged(CIR.Import.Idx){}, .is_function_boundary = is_function_boundary, + .associated_type_name = null, }; } @@ -43,8 +97,17 @@ pub fn init(is_function_boundary: bool) Scope { pub fn deinit(self: *Scope, gpa: std.mem.Allocator) void { self.idents.deinit(gpa); self.aliases.deinit(gpa); - self.type_decls.deinit(gpa); + + // Deinit forward reference arraylists + var forward_iter = self.forward_references.valueIterator(); + while (forward_iter.next()) |forward_ref| { + forward_ref.reference_regions.deinit(gpa); + } + self.forward_references.deinit(gpa); + + self.type_bindings.deinit(gpa); self.type_vars.deinit(gpa); + self.type_var_aliases.deinit(gpa); self.module_aliases.deinit(gpa); self.exposed_items.deinit(gpa); self.imported_modules.deinit(gpa); @@ -78,9 +141,28 @@ pub const TypeVarLookupResult = union(enum) { not_found: void, }; +/// Result of looking up a type variable alias +pub const TypeVarAliasLookupResult = union(enum) { + found: TypeVarAliasBinding, + not_found: void, +}; + +/// Result of introducing a type variable alias +pub const TypeVarAliasIntroduceResult = union(enum) { + success: void, + shadowing_warning: TypeVarAliasBinding, // The type var alias that was shadowed + already_in_scope: TypeVarAliasBinding, // The type var alias already exists in this scope +}; + +/// Information about a module alias +pub const ModuleAliasInfo = struct { + module_name: Ident.Idx, + is_package_qualified: bool, +}; + /// Result of looking up a module alias pub const ModuleAliasLookupResult = union(enum) { - found: Ident.Idx, + found: ModuleAliasInfo, not_found: void, }; @@ -128,8 +210,8 @@ pub const TypeVarIntroduceResult = union(enum) { /// Result of introducing a module alias pub const ModuleAliasIntroduceResult = union(enum) { success: void, - shadowing_warning: Ident.Idx, // The module alias that was shadowed - already_in_scope: Ident.Idx, // The module alias already exists in this scope + shadowing_warning: ModuleAliasInfo, // The module alias that was shadowed + already_in_scope: ModuleAliasInfo, // The module alias already exists in this scope }; /// Result of introducing an exposed item @@ -152,20 +234,18 @@ pub const ImportedModuleIntroduceResult = union(enum) { }; /// Item kinds in a scope -pub const ItemKind = enum { ident, alias, type_decl, type_var, module_alias, exposed_item }; +pub const ItemKind = enum { ident, alias, type_var, module_alias, exposed_item }; /// Get the appropriate map for the given item kind pub fn items(scope: *Scope, comptime item_kind: ItemKind) switch (item_kind) { .ident, .alias => *std.AutoHashMapUnmanaged(Ident.Idx, CIR.Pattern.Idx), - .type_decl => *std.AutoHashMapUnmanaged(Ident.Idx, CIR.Statement.Idx), .type_var => *std.AutoHashMapUnmanaged(Ident.Idx, CIR.TypeAnno.Idx), - .module_alias => *std.AutoHashMapUnmanaged(Ident.Idx, Ident.Idx), + .module_alias => *std.AutoHashMapUnmanaged(Ident.Idx, ModuleAliasInfo), .exposed_item => *std.AutoHashMapUnmanaged(Ident.Idx, ExposedItemInfo), } { return switch (item_kind) { .ident => &scope.idents, .alias => &scope.aliases, - .type_decl => &scope.type_decls, .type_var => &scope.type_vars, .module_alias => &scope.module_aliases, .exposed_item => &scope.exposed_items, @@ -175,15 +255,13 @@ pub fn items(scope: *Scope, comptime item_kind: ItemKind) switch (item_kind) { /// Get the appropriate map for the given item kind (const version) pub fn itemsConst(scope: *const Scope, comptime item_kind: ItemKind) switch (item_kind) { .ident, .alias => *const std.AutoHashMapUnmanaged(Ident.Idx, CIR.Pattern.Idx), - .type_decl => *const std.AutoHashMapUnmanaged(Ident.Idx, CIR.Statement.Idx), .type_var => *const std.AutoHashMapUnmanaged(Ident.Idx, CIR.TypeAnno.Idx), - .module_alias => *const std.AutoHashMapUnmanaged(Ident.Idx, Ident.Idx), + .module_alias => *const std.AutoHashMapUnmanaged(Ident.Idx, ModuleAliasInfo), .exposed_item => *const std.AutoHashMapUnmanaged(Ident.Idx, ExposedItemInfo), } { return switch (item_kind) { .ident => &scope.idents, .alias => &scope.aliases, - .type_decl => &scope.type_decls, .type_var => &scope.type_vars, .module_alias => &scope.module_aliases, .exposed_item => &scope.exposed_items, @@ -193,9 +271,8 @@ pub fn itemsConst(scope: *const Scope, comptime item_kind: ItemKind) switch (ite /// Put an item in the scope, panics on OOM pub fn put(scope: *Scope, gpa: std.mem.Allocator, comptime item_kind: ItemKind, name: Ident.Idx, value: switch (item_kind) { .ident, .alias => CIR.Pattern.Idx, - .type_decl => CIR.Statement.Idx, .type_var => CIR.TypeAnno.Idx, - .module_alias => Ident.Idx, + .module_alias => ModuleAliasInfo, .exposed_item => ExposedItemInfo, }) std.mem.Allocator.Error!void { try scope.items(item_kind).put(gpa, name, value); @@ -209,13 +286,14 @@ pub fn introduceTypeDecl( type_decl: CIR.Statement.Idx, parent_lookup_fn: ?fn (Ident.Idx) ?CIR.Statement.Idx, ) std.mem.Allocator.Error!TypeIntroduceResult { - // Check if already exists in current scope by comparing text content - var iter = scope.type_decls.iterator(); - while (iter.next()) |entry| { - if (name.idx == entry.key_ptr.idx) { - // Type redeclaration is an error, not just a warning - return TypeIntroduceResult{ .redeclared_error = entry.value_ptr.* }; - } + // Check if type already exists in this scope + if (scope.type_bindings.getPtr(name)) |existing| { + return switch (existing.*) { + .local_nominal => |stmt| TypeIntroduceResult{ .redeclared_error = stmt }, + .local_alias => |stmt| TypeIntroduceResult{ .type_alias_redeclared = stmt }, + .associated_nominal => |stmt| TypeIntroduceResult{ .nominal_type_redeclared = stmt }, + .external_nominal => TypeIntroduceResult{ .nominal_type_redeclared = type_decl }, + }; } // Check for shadowing in parent scopes and issue warnings @@ -224,7 +302,8 @@ pub fn introduceTypeDecl( shadowed_stmt = lookup_fn(name); } - try scope.put(gpa, .type_decl, name, type_decl); + // Add type binding (single source of truth) + try scope.type_bindings.put(gpa, name, TypeBinding{ .local_nominal = type_decl }); if (shadowed_stmt) |stmt| { return TypeIntroduceResult{ .shadowing_warning = stmt }; @@ -233,19 +312,17 @@ pub fn introduceTypeDecl( return TypeIntroduceResult{ .success = {} }; } -/// Lookup a type declaration in the scope hierarchy -/// TODO: Optimize lookup performance - currently O(n) due to text comparison -/// TODO: Consider caching or using a more efficient data structure for type lookup -/// TODO: Support for nominal vs structural type distinction (future := operator) -pub fn lookupTypeDecl(scope: *const Scope, name: Ident.Idx) TypeLookupResult { - // Search by comparing text content, not identifier index - var iter = scope.type_decls.iterator(); - while (iter.next()) |entry| { - if (name.idx == entry.key_ptr.idx) { - return TypeLookupResult{ .found = entry.value_ptr.* }; - } - } - return TypeLookupResult{ .not_found = {} }; +/// Introduce an unqualified type alias (for associated types) +/// Maps an unqualified name to a fully qualified type declaration +pub fn introduceTypeAlias( + scope: *Scope, + gpa: std.mem.Allocator, + unqualified_name: Ident.Idx, + qualified_type_decl: CIR.Statement.Idx, +) !void { + try scope.type_bindings.put(gpa, unqualified_name, TypeBinding{ + .associated_nominal = qualified_type_decl, + }); } /// Update an existing type declaration in the scope @@ -257,17 +334,17 @@ pub fn updateTypeDecl( name: Ident.Idx, new_type_decl: CIR.Statement.Idx, ) std.mem.Allocator.Error!void { - // Find the existing entry by comparing text content - var iter = scope.type_decls.iterator(); - while (iter.next()) |entry| { - if (name.idx == entry.key_ptr.idx) { - // Update the existing entry with the new statement index - entry.value_ptr.* = new_type_decl; - return; - } + if (scope.type_bindings.getPtr(name)) |binding_ptr| { + const current = binding_ptr.*; + binding_ptr.* = switch (current) { + .local_nominal => TypeBinding{ .local_nominal = new_type_decl }, + .local_alias => TypeBinding{ .local_alias = new_type_decl }, + .associated_nominal => TypeBinding{ .associated_nominal = new_type_decl }, + .external_nominal => current, + }; + } else { + try scope.type_bindings.put(gpa, name, TypeBinding{ .local_nominal = new_type_decl }); } - // If not found, add it as a new entry - try scope.put(gpa, .type_decl, name, new_type_decl); } /// Introduce a type variable into the scope @@ -314,9 +391,61 @@ pub fn lookupTypeVar(scope: *const Scope, name: Ident.Idx) TypeVarLookupResult { return TypeVarLookupResult{ .not_found = {} }; } +/// Look up a type variable alias in this scope (for static dispatch on type vars) +pub fn lookupTypeVarAlias(scope: *const Scope, name: Ident.Idx) TypeVarAliasLookupResult { + // Search by comparing .idx values (integer index into string interner) + var iter = scope.type_var_aliases.iterator(); + while (iter.next()) |entry| { + if (name.idx == entry.key_ptr.idx) { + return TypeVarAliasLookupResult{ .found = entry.value_ptr.* }; + } + } + return TypeVarAliasLookupResult{ .not_found = {} }; +} + +/// Introduce a type variable alias into this scope (for static dispatch on type vars) +pub fn introduceTypeVarAlias( + scope: *Scope, + gpa: std.mem.Allocator, + alias_name: Ident.Idx, + type_var_name: Ident.Idx, + type_var_anno: CIR.TypeAnno.Idx, + statement_idx: CIR.Statement.Idx, + parent_lookup_fn: ?*const fn (Ident.Idx) ?TypeVarAliasBinding, +) std.mem.Allocator.Error!TypeVarAliasIntroduceResult { + // Check if already exists in current scope by comparing text content + var iter = scope.type_var_aliases.iterator(); + while (iter.next()) |entry| { + if (alias_name.idx == entry.key_ptr.idx) { + // Type var alias already exists in this scope + return TypeVarAliasIntroduceResult{ .already_in_scope = entry.value_ptr.* }; + } + } + + // Check for shadowing in parent scopes + var shadowed_alias: ?TypeVarAliasBinding = null; + if (parent_lookup_fn) |lookup_fn| { + shadowed_alias = lookup_fn(alias_name); + } + + const binding = TypeVarAliasBinding{ + .type_var_name = type_var_name, + .type_var_anno = type_var_anno, + .statement_idx = statement_idx, + }; + + try scope.type_var_aliases.put(gpa, alias_name, binding); + + if (shadowed_alias) |shadowed| { + return TypeVarAliasIntroduceResult{ .shadowing_warning = shadowed }; + } + + return TypeVarAliasIntroduceResult{ .success = {} }; +} + /// Look up a module alias in this scope pub fn lookupModuleAlias(scope: *const Scope, name: Ident.Idx) ModuleAliasLookupResult { - // Search by comparing text content, not identifier index + // Search by comparing .idx values (integer index into string interner) var iter = scope.module_aliases.iterator(); while (iter.next()) |entry| { if (name.idx == entry.key_ptr.idx) { @@ -332,7 +461,8 @@ pub fn introduceModuleAlias( gpa: std.mem.Allocator, alias_name: Ident.Idx, module_name: Ident.Idx, - parent_lookup_fn: ?fn (Ident.Idx) ?Ident.Idx, + is_package_qualified: bool, + parent_lookup_fn: ?fn (Ident.Idx) ?ModuleAliasInfo, ) std.mem.Allocator.Error!ModuleAliasIntroduceResult { // Check if already exists in current scope by comparing text content var iter = scope.module_aliases.iterator(); @@ -344,15 +474,20 @@ pub fn introduceModuleAlias( } // Check for shadowing in parent scopes - var shadowed_module: ?Ident.Idx = null; + var shadowed_module: ?ModuleAliasInfo = null; if (parent_lookup_fn) |lookup_fn| { shadowed_module = lookup_fn(alias_name); } - try scope.put(gpa, .module_alias, alias_name, module_name); + const module_info = ModuleAliasInfo{ + .module_name = module_name, + .is_package_qualified = is_package_qualified, + }; - if (shadowed_module) |module| { - return ModuleAliasIntroduceResult{ .shadowing_warning = module }; + try scope.put(gpa, .module_alias, alias_name, module_info); + + if (shadowed_module) |info| { + return ModuleAliasIntroduceResult{ .shadowing_warning = info }; } return ModuleAliasIntroduceResult{ .success = {} }; diff --git a/src/canonicalize/Statement.zig b/src/canonicalize/Statement.zig index 13e785ba31..bd91c32519 100644 --- a/src/canonicalize/Statement.zig +++ b/src/canonicalize/Statement.zig @@ -40,6 +40,26 @@ pub const Statement = union(enum) { expr: Expr.Idx, anno: ?Annotation.Idx, }, + /// A generalized declaration (for lambdas and number literals only). + /// These bindings use let-polymorphism and can be instantiated at different types. + /// + /// ```roc + /// id = |x| x # generalized - can be used polymorphically + /// number = id(0) # works + /// empty_str = id("") # also works + /// + /// one = 1 # generalized - can be used polymorphically + /// u64_one = one + [].len() # `one` can be a U64 + /// two_point_one = one + 1.1 # `one` can also be Dec + /// ``` + /// + /// Other literals don't allow generalization - see + /// https://github.com/seanpm2001/Roc-Lang_RFCs/blob/main/0010-let-generalization-lets-not.md + s_decl_gen: struct { + pattern: Pattern.Idx, + expr: Expr.Idx, + anno: ?Annotation.Idx, + }, /// A rebindable declaration using the "var" keyword. /// /// Not valid at the top level of a module. @@ -109,6 +129,20 @@ pub const Statement = union(enum) { expr: Expr.Idx, body: Expr.Idx, }, + /// A block of code that will run repeatedly while a condition is true. + /// + /// Not valid at the top level of a module + /// + /// ```roc + /// while $count < 10 { + /// print!($count.toStr()) + /// $count = $count + 1 + /// } + /// ``` + s_while: struct { + cond: Expr.Idx, + body: Expr.Idx, + }, /// A early return of the enclosing function. /// /// Not valid at the top level of a module @@ -118,6 +152,9 @@ pub const Statement = union(enum) { /// ``` s_return: struct { expr: Expr.Idx, + /// The lambda this return belongs to (for type unification). + /// This is null if the return is outside a function (an error case). + lambda: ?Expr.Idx, }, /// Brings in another module for use in the current module, optionally exposing only certain members of that module. /// @@ -137,30 +174,56 @@ pub const Statement = union(enum) { header: CIR.TypeHeader.Idx, anno: CIR.TypeAnno.Idx, }, - /// A nominal type declaration, e.g., `Foo := (U64, Str)` + /// A nominal type declaration, e.g., `Foo := (U64, Str)` or `Foo :: (U64, Str)` /// /// Only valid at the top level of a module s_nominal_decl: struct { header: CIR.TypeHeader.Idx, anno: CIR.TypeAnno.Idx, + /// True if declared with :: (opaque), false if declared with := (nominal) + is_opaque: bool, }, /// A type annotation, declaring that the value referred to by an ident in the same scope should be a given type. /// /// ```roc - /// print! : Str => Result({}, [IOErr]) + /// print! : Str => Try({}, [IOErr]) /// ``` + /// + /// Typically an annotation will be stored on the `Def` and will not be + /// in the tree independently. But if there is an annotation without a + /// corresponding we represent it with this node s_type_anno: struct { name: Ident.Idx, anno: CIR.TypeAnno.Idx, where: ?CIR.WhereClause.Span, }, + /// A type variable alias within a block - enables static dispatch on type vars. + /// This binds an uppercase name to a type variable from the enclosing function signature, + /// allowing method calls like `Thing.method(arg)` that dispatch based on what the type + /// variable resolves to at runtime. + /// + /// ```roc + /// foo : thing -> Str + /// foo = |arg| + /// Thing : thing # Type var alias - binds `Thing` to the type variable `thing` + /// Thing.something(arg) # Static dispatch using the type var alias + /// ``` + s_type_var_alias: struct { + /// The alias name (e.g., "Thing") - uppercase identifier + alias_name: Ident.Idx, + /// The type variable name (e.g., "thing") - the lowercase type var being aliased + type_var_name: Ident.Idx, + /// Reference to the type annotation index for the type variable (from the enclosing scope) + type_var_anno: CIR.TypeAnno.Idx, + }, + s_runtime_error: struct { diagnostic: CIR.Diagnostic.Idx, }, pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: DataSpan }; + pub const Span = extern struct { span: DataSpan }; pub fn pushToSExprTree(self: *const @This(), env: *const ModuleEnv, tree: *SExprTree, stmt_idx: Statement.Idx) std.mem.Allocator.Error!void { switch (self.*) { @@ -176,6 +239,18 @@ pub const Statement = union(enum) { try tree.endNode(begin, attrs); }, + .s_decl_gen => |d| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("s-let"); + const region = env.store.getStatementRegion(stmt_idx); + try env.appendRegionInfoToSExprTreeFromRegion(tree, region); + const attrs = tree.beginNode(); + + try env.store.getPattern(d.pattern).pushToSExprTree(env, tree, d.pattern); + try env.store.getExpr(d.expr).pushToSExprTree(env, tree, d.expr); + + try tree.endNode(begin, attrs); + }, .s_var => |v| { const begin = tree.beginNode(); try tree.pushStaticAtom("s-var"); @@ -255,6 +330,18 @@ pub const Statement = union(enum) { try tree.endNode(begin, attrs); }, + .s_while => |s| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("s-while"); + const region = env.store.getStatementRegion(stmt_idx); + try env.appendRegionInfoToSExprTreeFromRegion(tree, region); + const attrs = tree.beginNode(); + + try env.store.getExpr(s.cond).pushToSExprTree(env, tree, s.cond); + try env.store.getExpr(s.body).pushToSExprTree(env, tree, s.body); + + try tree.endNode(begin, attrs); + }, .s_return => |s| { const begin = tree.beginNode(); try tree.pushStaticAtom("s-return"); @@ -342,6 +429,19 @@ pub const Statement = union(enum) { try tree.endNode(begin, attrs); }, + .s_type_var_alias => |s| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("s-type-var-alias"); + const region = env.store.getStatementRegion(stmt_idx); + try env.appendRegionInfoToSExprTreeFromRegion(tree, region); + try tree.pushStringPair("alias", env.getIdentText(s.alias_name)); + try tree.pushStringPair("type-var", env.getIdentText(s.type_var_name)); + const attrs = tree.beginNode(); + + try env.store.getTypeAnno(s.type_var_anno).pushToSExprTree(env, tree, s.type_var_anno); + + try tree.endNode(begin, attrs); + }, .s_runtime_error => |s| { const begin = tree.beginNode(); try tree.pushStaticAtom("s-runtime-error"); diff --git a/src/canonicalize/TypeAnnotation.zig b/src/canonicalize/TypeAnnotation.zig index ccc49325f2..42881fe9c3 100644 --- a/src/canonicalize/TypeAnnotation.zig +++ b/src/canonicalize/TypeAnnotation.zig @@ -19,6 +19,7 @@ const SExpr = base.SExpr; const SExprTree = base.SExprTree; const TypeVar = types.Var; const Expr = CIR.Expr; +const Statement = CIR.Statement; const IntValue = CIR.IntValue; const RocDec = builtins.RocDec; @@ -32,28 +33,44 @@ pub const TypeAnno = union(enum) { /// /// Examples: `List(Str)`, `Dict(String, Int)`, `Result(a, b)` apply: Apply, - /// Type application: applying a type constructor to arguments. - /// - /// Examples: `OtherModule.MyMap(String, Int)` - apply_external: ApplyExternal, /// Type variable: a placeholder type that can be unified with other types. /// /// Examples: `a`, `b`, `elem` in generic type signatures - ty_var: struct { + rigid_var: struct { name: Ident.Idx, // The variable name (e.g., "a", "b") }, + /// A rigid var that references another + /// + /// Examples: + /// + /// MyAlias(a) = List(a) + /// rigid_var ^ ^ rigid_var_lookup + /// + /// myFunction : a -> a + /// rigid_var ^ ^ rigid_var_lookup + rigid_var_lookup: struct { + ref: TypeAnno.Idx, // The variable name (e.g., "a", "b") + }, /// Inferred type `_` underscore: void, /// Basic type identifier: a concrete type name without arguments. /// /// Examples: `Str`, `U64`, `Bool` - ty: struct { - symbol: Ident.Idx, // The type name + lookup: struct { + name: Ident.Idx, // The type name + base: LocalOrExternal, }, /// Tag union type: a union of tags, possibly with payloads. /// /// Examples: `[Some(a), None]`, `[Red, Green, Blue]`, `[Cons(a, (List a)), Nil]` tag_union: TagUnion, + /// A tag in a gat union + /// + /// Examples: `Some(a)`, `None` + tag: struct { + name: Ident.Idx, // The tag name + args: TypeAnno.Span, // The tag arguments + }, /// Tuple type: a fixed-size collection of heterogeneous types. /// /// Examples: `(Str, U64)`, `(a, b, c)` @@ -72,13 +89,6 @@ pub const TypeAnno = union(enum) { parens: struct { anno: TypeAnno.Idx, // The type inside the parentheses }, - /// External type lookup: references a type from another module via external declaration. - /// - /// Examples: `Json.Value`, `Http.Request` - types that will be resolved when dependencies are available - ty_lookup_external: struct { - module_idx: CIR.Import.Idx, - target_node_idx: u16, - }, /// Malformed type annotation: represents a type that couldn't be parsed correctly. /// This follows the "Inform Don't Block" principle - compilation continues with /// an error marker that will be reported to the user. @@ -86,8 +96,13 @@ pub const TypeAnno = union(enum) { diagnostic: CIR.Diagnostic.Idx, // The error that occurred }, - pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: DataSpan }; + pub const Idx = enum(u32) { + /// Placeholder value indicating the anno hasn't been set yet. + /// Used during forward reference resolution. + placeholder = 0, + _, + }; + pub const Span = extern struct { span: DataSpan }; pub fn pushToSExprTree(self: *const @This(), ir: *const ModuleEnv, tree: *SExprTree, type_anno_idx: TypeAnno.Idx) std.mem.Allocator.Error!void { switch (self.*) { @@ -96,9 +111,39 @@ pub const TypeAnno = union(enum) { try tree.pushStaticAtom("ty-apply"); const region = ir.store.getTypeAnnoRegion(type_anno_idx); try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); - try tree.pushStringPair("symbol", ir.getIdentText(a.symbol)); - const attrs = tree.beginNode(); + try tree.pushStringPair("name", ir.getIdentText(a.name)); + switch (a.base) { + .builtin => |_| { + const field_begin = tree.beginNode(); + try tree.pushStaticAtom("builtin"); + const field_attrs = tree.beginNode(); + try tree.endNode(field_begin, field_attrs); + }, + .local => |_| { + const field_begin = tree.beginNode(); + try tree.pushStaticAtom("local"); + const field_attrs = tree.beginNode(); + try tree.endNode(field_begin, field_attrs); + }, + .external => |external| { + const module_idx_int = @intFromEnum(external.module_idx); + std.debug.assert(module_idx_int < ir.imports.imports.items.items.len); + const string_lit_idx = ir.imports.imports.items.items[module_idx_int]; + const module_name = ir.common.strings.get(string_lit_idx); + // Special case: Builtin module is an implementation detail, print as (builtin) + if (std.mem.eql(u8, module_name, "Builtin")) { + const field_begin = tree.beginNode(); + try tree.pushStaticAtom("builtin"); + const field_attrs = tree.beginNode(); + try tree.endNode(field_begin, field_attrs); + } else { + try tree.pushStringPair("external-module", module_name); + } + }, + } + + const attrs = tree.beginNode(); const args_slice = ir.store.sliceTypeAnnos(a.args); for (args_slice) |arg_idx| { try ir.store.getTypeAnno(arg_idx).pushToSExprTree(ir, tree, arg_idx); @@ -106,34 +151,22 @@ pub const TypeAnno = union(enum) { try tree.endNode(begin, attrs); }, - .apply_external => |a| { + .rigid_var => |tv| { const begin = tree.beginNode(); - try tree.pushStaticAtom("ty-apply-external"); - const region = ir.store.getTypeAnnoRegion(type_anno_idx); - try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); - const attrs = tree.beginNode(); - - // Add module index - var buf: [32]u8 = undefined; - const module_idx_str = std.fmt.bufPrint(&buf, "{}", .{@intFromEnum(a.module_idx)}) catch unreachable; - try tree.pushStringPair("module-idx", module_idx_str); - - // Add target node index - var buf2: [32]u8 = undefined; - const target_idx_str = std.fmt.bufPrint(&buf2, "{}", .{a.target_node_idx}) catch unreachable; - try tree.pushStringPair("target-node-idx", target_idx_str); - - try tree.endNode(begin, attrs); - }, - .ty_var => |tv| { - const begin = tree.beginNode(); - try tree.pushStaticAtom("ty-var"); + try tree.pushStaticAtom("ty-rigid-var"); const region = ir.store.getTypeAnnoRegion(type_anno_idx); try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); try tree.pushStringPair("name", ir.getIdentText(tv.name)); const attrs = tree.beginNode(); try tree.endNode(begin, attrs); }, + .rigid_var_lookup => |rv_lookup| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("ty-rigid-var-lookup"); + try ir.store.getTypeAnno(rv_lookup.ref).pushToSExprTree(ir, tree, rv_lookup.ref); + const attrs = tree.beginNode(); + try tree.endNode(begin, attrs); + }, .underscore => |_| { const begin = tree.beginNode(); try tree.pushStaticAtom("ty-underscore"); @@ -142,12 +175,43 @@ pub const TypeAnno = union(enum) { const attrs = tree.beginNode(); try tree.endNode(begin, attrs); }, - .ty => |t| { + .lookup => |t| { const begin = tree.beginNode(); - try tree.pushStaticAtom("ty"); + try tree.pushStaticAtom("ty-lookup"); const region = ir.store.getTypeAnnoRegion(type_anno_idx); try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); - try tree.pushStringPair("name", ir.getIdentText(t.symbol)); + try tree.pushStringPair("name", ir.getIdentText(t.name)); + + switch (t.base) { + .builtin => |_| { + const field_begin = tree.beginNode(); + try tree.pushStaticAtom("builtin"); + const field_attrs = tree.beginNode(); + try tree.endNode(field_begin, field_attrs); + }, + .local => |_| { + const field_begin = tree.beginNode(); + try tree.pushStaticAtom("local"); + const field_attrs = tree.beginNode(); + try tree.endNode(field_begin, field_attrs); + }, + .external => |external| { + const module_idx_int = @intFromEnum(external.module_idx); + std.debug.assert(module_idx_int < ir.imports.imports.items.items.len); + const string_lit_idx = ir.imports.imports.items.items[module_idx_int]; + const module_name = ir.common.strings.get(string_lit_idx); + // Special case: Builtin module is an implementation detail, print as (builtin) + if (std.mem.eql(u8, module_name, "Builtin")) { + const field_begin = tree.beginNode(); + try tree.pushStaticAtom("builtin"); + const field_attrs = tree.beginNode(); + try tree.endNode(field_begin, field_attrs); + } else { + try tree.pushStringPair("external-module", module_name); + } + }, + } + const attrs = tree.beginNode(); try tree.endNode(begin, attrs); }, @@ -169,6 +233,21 @@ pub const TypeAnno = union(enum) { try tree.endNode(begin, attrs); }, + .tag => |t| { + const begin = tree.beginNode(); + try tree.pushStaticAtom("ty-tag-name"); + const region = ir.store.getTypeAnnoRegion(type_anno_idx); + + try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); + try tree.pushStringPair("name", ir.getIdentText(t.name)); + + const attrs = tree.beginNode(); + const args_slice = ir.store.sliceTypeAnnos(t.args); + for (args_slice) |tag_idx| { + try ir.store.getTypeAnno(tag_idx).pushToSExprTree(ir, tree, tag_idx); + } + try tree.endNode(begin, attrs); + }, .tuple => |t| { const begin = tree.beginNode(); try tree.pushStaticAtom("ty-tuple"); @@ -234,25 +313,6 @@ pub const TypeAnno = union(enum) { try tree.endNode(begin, attrs); }, - .ty_lookup_external => |tle| { - const begin = tree.beginNode(); - try tree.pushStaticAtom("ty-lookup-external"); - const region = ir.store.getTypeAnnoRegion(type_anno_idx); - try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); - const attrs = tree.beginNode(); - - // Add module index - var buf: [32]u8 = undefined; - const module_idx_str = std.fmt.bufPrint(&buf, "{}", .{@intFromEnum(tle.module_idx)}) catch unreachable; - try tree.pushStringPair("module-idx", module_idx_str); - - // Add target node index - var buf2: [32]u8 = undefined; - const target_idx_str = std.fmt.bufPrint(&buf2, "{}", .{tle.target_node_idx}) catch unreachable; - try tree.pushStringPair("target-node-idx", target_idx_str); - - try tree.endNode(begin, attrs); - }, .malformed => |_| { const begin = tree.beginNode(); try tree.pushStaticAtom("ty-malformed"); @@ -270,19 +330,28 @@ pub const TypeAnno = union(enum) { ty: TypeAnno.Idx, pub const Idx = enum(u32) { _ }; - pub const Span = struct { span: DataSpan }; + pub const Span = extern struct { span: DataSpan }; + }; + + /// Either a locally declare type, or an external type + pub const LocalOrExternal = union(enum) { + builtin: Builtin, + local: struct { + decl_idx: Statement.Idx, + }, + external: struct { + module_idx: CIR.Import.Idx, + target_node_idx: u16, + }, + + // Just the tag of this union enum + pub const Tag = std.meta.Tag(@This()); }; /// A type application in a type annotation pub const Apply = struct { - symbol: Ident.Idx, // The type constructor being applied (e.g., "List", "Dict") - args: TypeAnno.Span, // The type arguments (e.g., [Str], [String, Int]) - }; - - /// A type application of an external type in a type annotation - pub const ApplyExternal = struct { - module_idx: CIR.Import.Idx, - target_node_idx: u16, + name: Ident.Idx, // The type name + base: LocalOrExternal, // Reference to the type args: TypeAnno.Span, // The type arguments (e.g., [Str], [String, Int]) }; @@ -296,6 +365,7 @@ pub const TypeAnno = union(enum) { /// A record in a type annotation pub const Record = struct { fields: RecordField.Span, // The field definitions + ext: ?TypeAnno.Idx, // Optional extension variable for open records }; /// A tag union in a type annotation @@ -308,4 +378,89 @@ pub const TypeAnno = union(enum) { pub const Tuple = struct { elems: TypeAnno.Span, // The types of each tuple element }; + + /// A builtin type + pub const Builtin = enum { + list, + box, + num, + u8, + u16, + u32, + u64, + u128, + i8, + i16, + i32, + i64, + i128, + f32, + f64, + dec, + + /// Convert a builtin type to it's name + pub fn toBytes(self: @This()) []const u8 { + switch (self) { + .list => return "List", + .box => return "Box", + .num => return "Num", + .u8 => return "U8", + .u16 => return "U16", + .u32 => return "U32", + .u64 => return "U64", + .u128 => return "U128", + .i8 => return "I8", + .i16 => return "I16", + .i32 => return "I32", + .i64 => return "I64", + .i128 => return "I128", + .f32 => return "F32", + .f64 => return "F64", + .dec => return "Dec", + } + } + + /// Convert a type name string to the corresponding builtin type + pub fn fromBytes(bytes: []const u8) ?@This() { + if (std.mem.eql(u8, bytes, "List")) return .list; + if (std.mem.eql(u8, bytes, "Box")) return .box; + if (std.mem.eql(u8, bytes, "Num")) return .num; + if (std.mem.eql(u8, bytes, "U8")) return .u8; + if (std.mem.eql(u8, bytes, "U16")) return .u16; + if (std.mem.eql(u8, bytes, "U32")) return .u32; + if (std.mem.eql(u8, bytes, "U64")) return .u64; + if (std.mem.eql(u8, bytes, "U128")) return .u128; + if (std.mem.eql(u8, bytes, "I8")) return .i8; + if (std.mem.eql(u8, bytes, "I16")) return .i16; + if (std.mem.eql(u8, bytes, "I32")) return .i32; + if (std.mem.eql(u8, bytes, "I64")) return .i64; + if (std.mem.eql(u8, bytes, "I128")) return .i128; + if (std.mem.eql(u8, bytes, "F32")) return .f32; + if (std.mem.eql(u8, bytes, "F64")) return .f64; + if (std.mem.eql(u8, bytes, "Dec")) return .dec; + return null; + } + + /// Check if an identifier index matches any builtin type name. + /// This is more efficient than fromBytes() as it compares indices directly. + pub fn isBuiltinTypeIdent(ident: base.Ident.Idx, idents: anytype) bool { + return ident == idents.list or + ident == idents.box or + ident == idents.str or + ident == idents.num or + ident == idents.u8 or + ident == idents.u16 or + ident == idents.u32 or + ident == idents.u64 or + ident == idents.u128 or + ident == idents.i8 or + ident == idents.i16 or + ident == idents.i32 or + ident == idents.i64 or + ident == idents.i128 or + ident == idents.f32 or + ident == idents.f64 or + ident == idents.dec; + } + }; }; diff --git a/src/canonicalize/mod.zig b/src/canonicalize/mod.zig index 33d42e21a4..6143a53448 100644 --- a/src/canonicalize/mod.zig +++ b/src/canonicalize/mod.zig @@ -8,6 +8,18 @@ pub const Can = @import("Can.zig"); pub const CIR = @import("CIR.zig"); /// The Module Environment after canonicalization (used also for type checking and serialization) pub const ModuleEnv = @import("ModuleEnv.zig"); +/// Scope management for canonicalization +pub const Scope = @import("Scope.zig"); +/// Dependency graph and SCC (Strongly Connected Components) analysis +pub const DependencyGraph = @import("DependencyGraph.zig"); +/// Hosted function compiler - replaces annotation-only with hosted lambdas +pub const HostedCompiler = @import("HostedCompiler.zig"); +/// Roc code emitter - converts CIR to valid Roc source code +pub const RocEmitter = @import("RocEmitter.zig"); +/// Monomorphizer - specializes polymorphic functions to concrete types +pub const Monomorphizer = @import("Monomorphizer.zig"); +/// Closure Transformer - transforms closures with captures into tagged values +pub const ClosureTransformer = @import("ClosureTransformer.zig"); test "compile tests" { std.testing.refAllDecls(@This()); @@ -25,13 +37,24 @@ test "compile tests" { std.testing.refAllDecls(@import("Statement.zig")); std.testing.refAllDecls(@import("TypeAnnotation.zig")); + std.testing.refAllDecls(@import("test/anno_only_test.zig")); std.testing.refAllDecls(@import("test/bool_test.zig")); std.testing.refAllDecls(@import("test/exposed_shadowing_test.zig")); std.testing.refAllDecls(@import("test/frac_test.zig")); + std.testing.refAllDecls(@import("test/if_statement_test.zig")); std.testing.refAllDecls(@import("test/import_validation_test.zig")); std.testing.refAllDecls(@import("test/int_test.zig")); std.testing.refAllDecls(@import("test/node_store_test.zig")); std.testing.refAllDecls(@import("test/import_store_test.zig")); std.testing.refAllDecls(@import("test/scope_test.zig")); std.testing.refAllDecls(@import("test/record_test.zig")); + std.testing.refAllDecls(@import("test/type_decl_stmt_test.zig")); + + // Backend tests (Roc emitter) + std.testing.refAllDecls(@import("RocEmitter.zig")); + std.testing.refAllDecls(@import("test/roc_emitter_test.zig")); + + // Monomorphization + std.testing.refAllDecls(@import("Monomorphizer.zig")); + std.testing.refAllDecls(@import("ClosureTransformer.zig")); } diff --git a/src/canonicalize/test/TestEnv.zig b/src/canonicalize/test/TestEnv.zig index 86c7a3c13a..66ff73283d 100644 --- a/src/canonicalize/test/TestEnv.zig +++ b/src/canonicalize/test/TestEnv.zig @@ -48,7 +48,7 @@ pub fn init(source: []const u8) !TestEnv { parse_ast.store.emptyScratch(); - try module_env.initCIRFields(gpa, "test"); + try module_env.initCIRFields("test"); can.* = try Can.init(module_env, parse_ast, null); diff --git a/src/canonicalize/test/anno_only_test.zig b/src/canonicalize/test/anno_only_test.zig new file mode 100644 index 0000000000..ad8799c897 --- /dev/null +++ b/src/canonicalize/test/anno_only_test.zig @@ -0,0 +1,19 @@ +//! Tests for standalone type annotation canonicalization. +//! +//! This module contains unit tests that verify the e_anno_only expression variant +//! works correctly in the compiler's canonical internal representation (CIR). + +const std = @import("std"); +const testing = std.testing; +const CIR = @import("../CIR.zig"); + +test "e_anno_only expression variant exists" { + // Create an e_anno_only expression + const expr = CIR.Expr{ .e_anno_only = .{} }; + + // Verify it's the correct variant + switch (expr) { + .e_anno_only => {}, + else => return error.WrongExprVariant, + } +} diff --git a/src/canonicalize/test/bool_test.zig b/src/canonicalize/test/bool_test.zig index e3f0c4794a..86d89d72b9 100644 --- a/src/canonicalize/test/bool_test.zig +++ b/src/canonicalize/test/bool_test.zig @@ -21,16 +21,10 @@ test "canonicalize True as Bool" { // Get the expression const expr = test_env.getCanonicalExpr(canonical_expr.get_idx()); - // Check if it's a nominal expression (Bool) - try testing.expectEqual(.e_nominal, std.meta.activeTag(expr)); - - // The backing expression should be a tag - const backing_expr = test_env.module_env.store.getExpr(expr.e_nominal.backing_expr); - try testing.expectEqual(.e_tag, std.meta.activeTag(backing_expr)); - try testing.expectEqual(CIR.Expr.NominalBackingType.tag, expr.e_nominal.backing_type); + try testing.expectEqual(.e_tag, std.meta.activeTag(expr)); // The tag should be "True" - const tag_name = test_env.getIdent(backing_expr.e_tag.name); + const tag_name = test_env.getIdent(expr.e_tag.name); try testing.expectEqualStrings("True", tag_name); } @@ -44,16 +38,10 @@ test "canonicalize False as Bool" { // Get the expression const expr = test_env.getCanonicalExpr(canonical_expr.get_idx()); - // Check if it's a nominal expression (Bool) - try testing.expectEqual(.e_nominal, std.meta.activeTag(expr)); - - // The backing expression should be a tag - const backing_expr = test_env.module_env.store.getExpr(expr.e_nominal.backing_expr); - try testing.expectEqual(.e_tag, std.meta.activeTag(backing_expr)); - try testing.expectEqual(CIR.Expr.NominalBackingType.tag, expr.e_nominal.backing_type); + try testing.expectEqual(.e_tag, std.meta.activeTag(expr)); // The tag should be "False" - const tag_name = test_env.getIdent(backing_expr.e_tag.name); + const tag_name = test_env.getIdent(expr.e_tag.name); try testing.expectEqualStrings("False", tag_name); } diff --git a/src/canonicalize/test/exposed_shadowing_test.zig b/src/canonicalize/test/exposed_shadowing_test.zig index 35d7a70747..be8351c175 100644 --- a/src/canonicalize/test/exposed_shadowing_test.zig +++ b/src/canonicalize/test/exposed_shadowing_test.zig @@ -27,7 +27,7 @@ test "exposed but not implemented - values" { var env = try ModuleEnv.init(allocator, source); defer env.deinit(); - try env.initCIRFields(allocator, "Test"); + try env.initCIRFields("Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); @@ -39,8 +39,8 @@ test "exposed but not implemented - values" { // Check that we have an "exposed but not implemented" diagnostic for 'bar' var found_bar_error = false; - for (0..env.store.scratch_diagnostics.top()) |i| { - const diag_idx = env.store.scratch_diagnostics.items.items[i]; + for (0..env.store.scratch.?.diagnostics.top()) |i| { + const diag_idx = env.store.scratch.?.diagnostics.items.items[i]; const diag = env.store.getDiagnostic(diag_idx); switch (diag) { .exposed_but_not_implemented => |d| { @@ -66,7 +66,7 @@ test "exposed but not implemented - types" { var env = try ModuleEnv.init(allocator, source); defer env.deinit(); - try env.initCIRFields(allocator, "Test"); + try env.initCIRFields("Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); @@ -78,8 +78,8 @@ test "exposed but not implemented - types" { // Check that we have an "exposed but not implemented" diagnostic for 'OtherType' var found_other_type_error = false; - for (0..env.store.scratch_diagnostics.top()) |i| { - const diag_idx = env.store.scratch_diagnostics.items.items[i]; + for (0..env.store.scratch.?.diagnostics.top()) |i| { + const diag_idx = env.store.scratch.?.diagnostics.items.items[i]; const diag = env.store.getDiagnostic(diag_idx); switch (diag) { .exposed_but_not_implemented => |d| { @@ -105,7 +105,7 @@ test "redundant exposed entries" { ; var env = try ModuleEnv.init(allocator, source); defer env.deinit(); - try env.initCIRFields(allocator, "Test"); + try env.initCIRFields("Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); var czer = try Can.init(&env, &ast, null); @@ -116,8 +116,8 @@ test "redundant exposed entries" { // Check that we have redundant exposed warnings var found_foo_redundant = false; var found_bar_redundant = false; - for (0..env.store.scratch_diagnostics.top()) |i| { - const diag_idx = env.store.scratch_diagnostics.items.items[i]; + for (0..env.store.scratch.?.diagnostics.top()) |i| { + const diag_idx = env.store.scratch.?.diagnostics.items.items[i]; const diag = env.store.getDiagnostic(diag_idx); switch (diag) { .redundant_exposed => |d| { @@ -148,7 +148,7 @@ test "shadowing with exposed items" { ; var env = try ModuleEnv.init(allocator, source); defer env.deinit(); - try env.initCIRFields(allocator, "Test"); + try env.initCIRFields("Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); var czer = try Can.init(&env, &ast, null); @@ -158,8 +158,8 @@ test "shadowing with exposed items" { .canonicalizeFile(); // Check that we have shadowing warnings var shadowing_count: usize = 0; - for (0..env.store.scratch_diagnostics.top()) |i| { - const diag_idx = env.store.scratch_diagnostics.items.items[i]; + for (0..env.store.scratch.?.diagnostics.top()) |i| { + const diag_idx = env.store.scratch.?.diagnostics.items.items[i]; const diag = env.store.getDiagnostic(diag_idx); switch (diag) { .shadowing_warning => shadowing_count += 1, @@ -181,7 +181,7 @@ test "shadowing non-exposed items" { ; var env = try ModuleEnv.init(allocator, source); defer env.deinit(); - try env.initCIRFields(allocator, "Test"); + try env.initCIRFields("Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); var czer = try Can.init(&env, &ast, null); @@ -191,8 +191,8 @@ test "shadowing non-exposed items" { .canonicalizeFile(); // Check that we still get shadowing warnings for non-exposed items var found_shadowing = false; - for (0..env.store.scratch_diagnostics.top()) |i| { - const diag_idx = env.store.scratch_diagnostics.items.items[i]; + for (0..env.store.scratch.?.diagnostics.top()) |i| { + const diag_idx = env.store.scratch.?.diagnostics.items.items[i]; const diag = env.store.getDiagnostic(diag_idx); switch (diag) { .shadowing_warning => |d| { @@ -221,7 +221,7 @@ test "exposed items correctly tracked across shadowing" { ; var env = try ModuleEnv.init(allocator, source); defer env.deinit(); - try env.initCIRFields(allocator, "Test"); + try env.initCIRFields("Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); var czer = try Can.init(&env, &ast, null); @@ -237,8 +237,8 @@ test "exposed items correctly tracked across shadowing" { var found_x_shadowing = false; var found_z_not_implemented = false; var found_unexpected_not_implemented = false; - for (0..env.store.scratch_diagnostics.top()) |i| { - const diag_idx = env.store.scratch_diagnostics.items.items[i]; + for (0..env.store.scratch.?.diagnostics.top()) |i| { + const diag_idx = env.store.scratch.?.diagnostics.items.items[i]; const diag = env.store.getDiagnostic(diag_idx); switch (diag) { .shadowing_warning => |d| { @@ -277,7 +277,7 @@ test "complex case with redundant, shadowing, and not implemented" { ; var env = try ModuleEnv.init(allocator, source); defer env.deinit(); - try env.initCIRFields(allocator, "Test"); + try env.initCIRFields("Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); var czer = try Can.init(&env, &ast, null); @@ -288,8 +288,8 @@ test "complex case with redundant, shadowing, and not implemented" { var found_a_redundant = false; var found_a_shadowing = false; var found_not_implemented = false; - for (0..env.store.scratch_diagnostics.top()) |i| { - const diag_idx = env.store.scratch_diagnostics.items.items[i]; + for (0..env.store.scratch.?.diagnostics.top()) |i| { + const diag_idx = env.store.scratch.?.diagnostics.items.items[i]; const diag = env.store.getDiagnostic(diag_idx); switch (diag) { .redundant_exposed => |d| { @@ -329,7 +329,7 @@ test "exposed_items is populated correctly" { ; var env = try ModuleEnv.init(allocator, source); defer env.deinit(); - try env.initCIRFields(allocator, "Test"); + try env.initCIRFields("Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); var czer = try Can.init(&env, &ast, null); @@ -339,15 +339,15 @@ test "exposed_items is populated correctly" { .canonicalizeFile(); // Check that exposed_items contains the correct number of items // The exposed items were added during canonicalization - // Should have exactly 3 entries (duplicates not stored) - try testing.expectEqual(@as(usize, 3), env.common.exposed_items.count()); - // Check that exposed_items contains all exposed items + // Should have exactly 2 value entries (duplicates not stored, types not included) + // Types are not stored in exposed_items - they are handled by the type system + try testing.expectEqual(@as(usize, 2), env.common.exposed_items.count()); + // Check that exposed_items contains all exposed values (not types) const foo_idx = env.common.idents.findByString("foo").?; const bar_idx = env.common.idents.findByString("bar").?; - const mytype_idx = env.common.idents.findByString("MyType").?; try testing.expect(env.common.exposed_items.containsById(env.gpa, @bitCast(foo_idx))); try testing.expect(env.common.exposed_items.containsById(env.gpa, @bitCast(bar_idx))); - try testing.expect(env.common.exposed_items.containsById(env.gpa, @bitCast(mytype_idx))); + // MyType is not in exposed_items because it's a type, not a value } test "exposed_items persists after canonicalization" { @@ -361,7 +361,7 @@ test "exposed_items persists after canonicalization" { ; var env = try ModuleEnv.init(allocator, source); defer env.deinit(); - try env.initCIRFields(allocator, "Test"); + try env.initCIRFields("Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); var czer = try Can.init(&env, &ast, null); @@ -391,7 +391,7 @@ test "exposed_items never has entries removed" { ; var env = try ModuleEnv.init(allocator, source); defer env.deinit(); - try env.initCIRFields(allocator, "Test"); + try env.initCIRFields("Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); var czer = try Can.init(&env, &ast, null); @@ -424,7 +424,7 @@ test "exposed_items handles identifiers with different attributes" { ; var env = try ModuleEnv.init(allocator, source); defer env.deinit(); - try env.initCIRFields(allocator, "Test"); + try env.initCIRFields("Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); var czer = try Can.init(&env, &ast, null); diff --git a/src/canonicalize/test/frac_test.zig b/src/canonicalize/test/frac_test.zig index bff445686d..d1f30a4ad3 100644 --- a/src/canonicalize/test/frac_test.zig +++ b/src/canonicalize/test/frac_test.zig @@ -8,7 +8,6 @@ const std = @import("std"); const base = @import("base"); const parse = @import("parse"); const compile = @import("compile"); -const types = @import("types"); const builtins = @import("builtins"); const Can = @import("../Can.zig"); @@ -28,61 +27,10 @@ test "fractional literal - basic decimal" { switch (expr) { .e_dec_small => |dec| { - try testing.expectEqual(dec.numerator, 314); - try testing.expectEqual(dec.denominator_power_of_ten, 2); - const expr_as_type_var: types.Var = @enumFromInt(@intFromEnum(canonical_expr.get_idx())); - const resolved = test_env.module_env.types.resolveVar(expr_as_type_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .num_poly => return error.UnexpectedNumPolyType, - .frac_poly => |poly| { - try testing.expect(poly.requirements.fits_in_dec); - }, - .frac_unbound => |requirements| { - try testing.expect(requirements.fits_in_dec); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - .flex_var => { - // It's an unbound type variable, which is also fine for literals - }, - else => { - std.debug.print("Unexpected content type: {}\n", .{resolved.desc.content}); - return error.UnexpectedContentType; - }, - } - }, - .e_frac_dec => |dec| { - _ = dec; - // Also accept e_frac_dec for decimal literals - const expr_as_type_var: types.Var = @enumFromInt(@intFromEnum(canonical_expr.get_idx())); - const resolved = test_env.module_env.types.resolveVar(expr_as_type_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .num_poly => return error.UnexpectedNumPolyType, - .frac_poly => |poly| { - try testing.expect(poly.requirements.fits_in_dec); - }, - .frac_unbound => |requirements| { - try testing.expect(requirements.fits_in_dec); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - .flex_var => { - // It's an unbound type variable, which is also fine for literals - }, - else => { - std.debug.print("Unexpected content type: {}\n", .{resolved.desc.content}); - return error.UnexpectedContentType; - }, - } + try testing.expectEqual(dec.value.numerator, 314); + try testing.expectEqual(dec.value.denominator_power_of_ten, 2); }, + .e_dec => {}, else => { std.debug.print("Unexpected expr type: {}\n", .{expr}); try testing.expect(false); // Should be dec_small or frac_dec @@ -102,89 +50,12 @@ test "fractional literal - scientific notation small" { .e_dec_small => |dec| { // Very small numbers may round to zero when parsed as small decimal // This is expected behavior when the value is too small for i16 representation - try testing.expectEqual(dec.numerator, 0); - - // Still check type requirements - const expr_as_type_var: types.Var = @enumFromInt(@intFromEnum(canonical_expr.get_idx())); - const resolved = test_env.module_env.types.resolveVar(expr_as_type_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .frac_poly => |poly| { - try testing.expect(poly.requirements.fits_in_f32); - try testing.expect(poly.requirements.fits_in_dec); - }, - .frac_unbound => |requirements| { - try testing.expect(requirements.fits_in_f32); - try testing.expect(requirements.fits_in_dec); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - .flex_var => { - // It's an unbound type variable, which is also fine for literals - }, - else => return error.UnexpectedContentType, - } + try testing.expectEqual(dec.value.numerator, 0); }, - .e_frac_dec => |frac| { - // Scientific notation can also be parsed as RocDec for exact representation - const expr_as_type_var: types.Var = @enumFromInt(@intFromEnum(canonical_expr.get_idx())); - const resolved = test_env.module_env.types.resolveVar(expr_as_type_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .num_poly => return error.UnexpectedNumPolyType, - .frac_poly => |poly| { - // 1.23e-10 is within f32 range, so it should fit (ignoring precision) - try testing.expect(poly.requirements.fits_in_f32); - try testing.expect(poly.requirements.fits_in_dec); - }, - .frac_unbound => |requirements| { - // 1.23e-10 is within f32 range, so it should fit (ignoring precision) - try testing.expect(requirements.fits_in_f32); - try testing.expect(requirements.fits_in_dec); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - .flex_var => { - // It's an unbound type variable, which is also fine for literals - }, - else => return error.UnexpectedContentType, - } + .e_dec => { // RocDec stores the value in a special format - _ = frac; }, .e_frac_f64 => |frac| { - // Or it might be parsed as f64 - const expr_as_type_var: types.Var = @enumFromInt(@intFromEnum(canonical_expr.get_idx())); - const resolved = test_env.module_env.types.resolveVar(expr_as_type_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .num_poly => return error.UnexpectedNumPolyType, - .frac_poly => |poly| { - // 1.23e-10 is within f32 range, so it should fit (ignoring precision) - try testing.expect(poly.requirements.fits_in_f32); - try testing.expect(poly.requirements.fits_in_dec); - }, - .frac_unbound => |requirements| { - // 1.23e-10 is within f32 range, so it should fit (ignoring precision) - try testing.expect(requirements.fits_in_f32); - try testing.expect(requirements.fits_in_dec); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - .flex_var => { - // It's an unbound type variable, which is also fine for literals - }, - else => return error.UnexpectedContentType, - } try testing.expectApproxEqAbs(frac.value, 1.23e-10, 1e-20); }, else => { @@ -204,27 +75,6 @@ test "fractional literal - scientific notation large (near f64 max)" { switch (expr) { .e_frac_f64 => |frac| { - const expr_as_type_var: types.Var = @enumFromInt(@intFromEnum(canonical_expr.get_idx())); - const resolved = test_env.module_env.types.resolveVar(expr_as_type_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .num_poly => return error.UnexpectedNumPolyType, - .frac_poly => |poly| { - try testing.expect(!poly.requirements.fits_in_dec); // Way out of Dec range - }, - .frac_unbound => |requirements| { - try testing.expect(!requirements.fits_in_dec); // Way out of Dec range - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - .flex_var => { - // It's an unbound type variable, which is also fine for literals - }, - else => return error.UnexpectedContentType, - } try testing.expectEqual(frac.value, 1e308); }, else => { @@ -264,12 +114,12 @@ test "fractional literal - negative zero" { switch (expr) { .e_dec_small => |small| { // dec_small doesn't preserve sign for -0.0 - try testing.expectEqual(small.numerator, 0); - try testing.expectEqual(small.denominator_power_of_ten, 0); + try testing.expectEqual(small.value.numerator, 0); + try testing.expectEqual(small.value.denominator_power_of_ten, 0); try testing.expect(true); // -0.0 fits in Dec try testing.expect(true); // -0.0 fits in F32 }, - .e_frac_dec => |frac| { + .e_dec => |frac| { try testing.expect(true); // -0.0 fits in Dec and F32 const f64_val = frac.value.toF64(); // RocDec may not preserve the sign bit for -0.0, so just check it's zero @@ -296,8 +146,8 @@ test "fractional literal - positive zero" { switch (expr) { .e_dec_small => |small| { - try testing.expectEqual(small.numerator, 0); - try testing.expectEqual(small.denominator_power_of_ten, 1); + try testing.expectEqual(small.value.numerator, 0); + try testing.expectEqual(small.value.denominator_power_of_ten, 1); try testing.expect(true); // 0.5 fits in F32 try testing.expect(true); // 0.5 fits in Dec }, @@ -373,7 +223,7 @@ test "fractional literal - scientific notation with capital E" { const expr = test_env.getCanonicalExpr(canonical_expr.get_idx()); switch (expr) { - .e_frac_dec => |frac| { + .e_dec => |frac| { try testing.expect(true); // 1e7 fits in Dec try testing.expect(true); // 2.5e10 is within f32 range try testing.expectApproxEqAbs(@as(f64, @floatFromInt(frac.value.num)) / std.math.pow(f64, 10, 18), 2.5e10, 1e-5); @@ -393,7 +243,7 @@ test "fractional literal - negative scientific notation" { const expr = test_env.getCanonicalExpr(canonical_expr.get_idx()); switch (expr) { - .e_frac_dec => |frac| { + .e_dec => |frac| { try testing.expect(true); // 1e-7 fits in Dec // -1.5e-5 may not round-trip perfectly through f32 // Let's just check the value is correct @@ -439,8 +289,8 @@ test "negative zero forced to f64 parsing" { switch (expr) { .e_dec_small => |small| { - try testing.expectEqual(small.numerator, 0); - try testing.expectEqual(small.denominator_power_of_ten, 0); + try testing.expectEqual(small.value.numerator, 0); + try testing.expectEqual(small.value.denominator_power_of_ten, 0); }, else => { try testing.expect(false); // Should be dec_small @@ -476,8 +326,8 @@ test "small dec - basic positive decimal" { switch (expr) { .e_dec_small => |dec| { - try testing.expectEqual(dec.numerator, 314); - try testing.expectEqual(dec.denominator_power_of_ten, 2); + try testing.expectEqual(dec.value.numerator, 314); + try testing.expectEqual(dec.value.denominator_power_of_ten, 2); try testing.expect(true); // 1.1 fits in Dec }, else => { @@ -496,10 +346,10 @@ test "negative zero preservation - uses f64" { switch (expr) { .e_dec_small => |small| { - try testing.expectEqual(small.numerator, 0); + try testing.expectEqual(small.value.numerator, 0); // Sign is lost with dec_small }, - .e_frac_dec => |frac| { + .e_dec => |frac| { // If it went through Dec path, check if sign is preserved if (std.mem.eql(u8, "-0.0", "-0.0")) { const f64_val = @as(f64, @floatFromInt(frac.value.num)) / std.math.pow(f64, 10, 18); @@ -523,8 +373,8 @@ test "negative zero with scientific notation - preserves sign via f64" { switch (expr) { .e_dec_small => |small| { - try testing.expectEqual(small.numerator, 0); - try testing.expectEqual(small.denominator_power_of_ten, 0); + try testing.expectEqual(small.value.numerator, 0); + try testing.expectEqual(small.value.denominator_power_of_ten, 0); }, else => { try testing.expect(false); // Should be dec_small @@ -542,11 +392,11 @@ test "small dec - positive zero" { switch (expr) { .e_dec_small => |dec| { - try testing.expectEqual(dec.numerator, 0); - try testing.expectEqual(dec.denominator_power_of_ten, 1); + try testing.expectEqual(dec.value.numerator, 0); + try testing.expectEqual(dec.value.denominator_power_of_ten, 1); // Verify positive zero - try testing.expectEqual(dec.numerator, 0); + try testing.expectEqual(dec.value.numerator, 0); }, else => { try testing.expect(false); // Should be dec_small @@ -564,8 +414,8 @@ test "small dec - precision preservation for 0.1" { switch (expr) { .e_dec_small => |dec| { - try testing.expectEqual(dec.numerator, 1); - try testing.expectEqual(dec.denominator_power_of_ten, 1); + try testing.expectEqual(dec.value.numerator, 1); + try testing.expectEqual(dec.value.denominator_power_of_ten, 1); }, else => { try testing.expect(false); // Should be dec_small @@ -583,8 +433,8 @@ test "small dec - trailing zeros" { switch (expr) { .e_dec_small => |dec| { - try testing.expectEqual(dec.numerator, 1100); - try testing.expectEqual(dec.denominator_power_of_ten, 3); + try testing.expectEqual(dec.value.numerator, 1100); + try testing.expectEqual(dec.value.denominator_power_of_ten, 3); }, else => { try testing.expect(false); // Should be dec_small @@ -602,8 +452,8 @@ test "small dec - negative number" { switch (expr) { .e_dec_small => |dec| { - try testing.expectEqual(dec.numerator, -525); - try testing.expectEqual(dec.denominator_power_of_ten, 2); + try testing.expectEqual(dec.value.numerator, -525); + try testing.expectEqual(dec.value.denominator_power_of_ten, 2); }, else => { try testing.expect(false); // Should be dec_small @@ -621,8 +471,8 @@ test "small dec - max i8 value" { switch (expr) { .e_dec_small => |dec| { - try testing.expectEqual(dec.numerator, 12799); - try testing.expectEqual(dec.denominator_power_of_ten, 2); + try testing.expectEqual(dec.value.numerator, 12799); + try testing.expectEqual(dec.value.denominator_power_of_ten, 2); }, else => { try testing.expect(false); // Should be dec_small @@ -640,8 +490,8 @@ test "small dec - min i8 value" { switch (expr) { .e_dec_small => |dec| { - try testing.expectEqual(dec.numerator, -1280); - try testing.expectEqual(dec.denominator_power_of_ten, 1); + try testing.expectEqual(dec.value.numerator, -1280); + try testing.expectEqual(dec.value.denominator_power_of_ten, 1); }, else => { try testing.expect(false); // Should be dec_small @@ -660,8 +510,8 @@ test "small dec - 128.0 now fits with new representation" { switch (expr) { .e_dec_small => |dec| { // With numerator/power representation, 128.0 = 1280/10^1 fits in i16 - try testing.expectEqual(dec.numerator, 1280); - try testing.expectEqual(dec.denominator_power_of_ten, 1); + try testing.expectEqual(dec.value.numerator, 1280); + try testing.expectEqual(dec.value.denominator_power_of_ten, 1); }, else => { try testing.expect(false); // Should be dec_small @@ -678,7 +528,7 @@ test "small dec - exceeds i16 range falls back to Dec" { const expr = test_env.getCanonicalExpr(canonical_expr.get_idx()); switch (expr) { - .e_frac_dec => { + .e_dec => { // Should fall back to Dec because 327680 > 32767 (max i16) try testing.expect(true); // 3.141 fits in Dec }, @@ -701,8 +551,8 @@ test "small dec - too many fractional digits falls back to Dec" { switch (expr) { .e_dec_small => |dec| { - try testing.expectEqual(dec.numerator, 1234); - try testing.expectEqual(dec.denominator_power_of_ten, 3); + try testing.expectEqual(dec.value.numerator, 1234); + try testing.expectEqual(dec.value.denominator_power_of_ten, 3); }, else => { try testing.expect(false); // Should be dec_small @@ -720,8 +570,8 @@ test "small dec - complex example 0.001" { switch (expr) { .e_dec_small => |dec| { - try testing.expectEqual(dec.numerator, 1); - try testing.expectEqual(dec.denominator_power_of_ten, 3); + try testing.expectEqual(dec.value.numerator, 1); + try testing.expectEqual(dec.value.denominator_power_of_ten, 3); }, else => { try testing.expect(false); // Should be dec_small @@ -740,8 +590,8 @@ test "small dec - negative example -0.05" { switch (expr) { .e_dec_small => |dec| { // -0.05 = -5 / 10^2 - try testing.expectEqual(dec.numerator, -5); - try testing.expectEqual(dec.denominator_power_of_ten, 2); + try testing.expectEqual(dec.value.numerator, -5); + try testing.expectEqual(dec.value.denominator_power_of_ten, 2); }, else => { try testing.expect(false); // Should be dec_small @@ -760,8 +610,8 @@ test "negative zero with scientific notation preserves sign" { switch (expr) { .e_dec_small => |small| { // With scientific notation, now uses dec_small and loses sign - try testing.expectEqual(small.numerator, 0); - try testing.expectEqual(small.denominator_power_of_ten, 0); + try testing.expectEqual(small.value.numerator, 0); + try testing.expectEqual(small.value.denominator_power_of_ten, 0); }, else => { try testing.expect(false); // Should be dec_small @@ -779,8 +629,8 @@ test "fractional literal - simple 1.0 uses small dec" { switch (expr) { .e_dec_small => |dec| { - try testing.expectEqual(dec.numerator, 10); - try testing.expectEqual(dec.denominator_power_of_ten, 1); + try testing.expectEqual(dec.value.numerator, 10); + try testing.expectEqual(dec.value.denominator_power_of_ten, 1); }, else => { try testing.expect(false); // Should be dec_small diff --git a/src/canonicalize/test/if_statement_test.zig b/src/canonicalize/test/if_statement_test.zig new file mode 100644 index 0000000000..4b9f476c44 --- /dev/null +++ b/src/canonicalize/test/if_statement_test.zig @@ -0,0 +1,50 @@ +//! Tests for if statements without else in statement position +const std = @import("std"); +const CIR = @import("../CIR.zig"); + +const testing = std.testing; + +const TestEnv = @import("TestEnv.zig").TestEnv; + +test "if without else in lambda block statement position should succeed" { + // This matches the pattern in Builtin.roc's List.is_eq: + // is_eq = |self, other| { + // if self.len() != other.len() { + // return False + // } + // True + // } + // The if without else is in statement position - its value is not used. + // + // The bug: When canonicalizing a module-level declaration's RHS, in_statement_position + // is set to false. The lambda body block should reset in_statement_position to true + // for its statements, but it doesn't. This causes the `if` statement to be incorrectly + // flagged as needing an `else` branch. + // + // To simulate this, we set in_statement_position = false before canonicalizing, + // which is what happens when canonicalizing the RHS of a module-level declaration. + const source = + \\|x, y| { + \\ if x != y { + \\ return False + \\ } + \\ True + \\} + ; + var test_env = try TestEnv.init(source); + defer test_env.deinit(); + + // Simulate the condition when canonicalizing a declaration's RHS: + // in_statement_position is set to false before entering the lambda + test_env.can.in_statement_position = false; + + const result = try test_env.canonicalizeExpr(); + + // Canonicalization should succeed + try testing.expect(result != null); + + // There should be no diagnostics (specifically no if_expr_without_else) + const diagnostics = try test_env.getDiagnostics(); + defer testing.allocator.free(diagnostics); + try testing.expectEqual(@as(usize, 0), diagnostics.len); +} diff --git a/src/canonicalize/test/import_store_test.zig b/src/canonicalize/test/import_store_test.zig index 7fe433db82..f23c08ac5e 100644 --- a/src/canonicalize/test/import_store_test.zig +++ b/src/canonicalize/test/import_store_test.zig @@ -9,56 +9,54 @@ const Import = CIR.Import; const StringLiteral = base.StringLiteral; const CompactWriter = collections.CompactWriter; +fn storeContainsModule(store: *const Import.Store, string_store: *const StringLiteral.Store, module_name: []const u8) bool { + for (store.imports.items.items) |string_idx| { + if (std.mem.eql(u8, string_store.get(string_idx), module_name)) { + return true; + } + } + return false; +} + test "Import.Store deduplicates module names" { const testing = std.testing; const gpa = testing.allocator; - var arena = std.heap.ArenaAllocator.init(gpa); - defer arena.deinit(); - const arena_allocator = arena.allocator(); - // Create a string store for interning module names - var string_store = try StringLiteral.Store.initCapacityBytes(arena_allocator, 1024); + var string_store = try StringLiteral.Store.initCapacityBytes(gpa, 1024); + defer string_store.deinit(gpa); - // Create import store var store = Import.Store.init(); + defer store.deinit(gpa); // Add the same module name multiple times - should deduplicate - const idx1 = try store.getOrPut(arena_allocator, &string_store, "test.Module"); - const idx2 = try store.getOrPut(arena_allocator, &string_store, "test.Module"); + const idx1 = try store.getOrPut(gpa, &string_store, "test.Module"); + const idx2 = try store.getOrPut(gpa, &string_store, "test.Module"); - // Should get the same index + // Should get the same index back (deduplication) try testing.expectEqual(idx1, idx2); try testing.expectEqual(@as(usize, 1), store.imports.len()); // Add a different module - should create a new entry - const idx3 = try store.getOrPut(arena_allocator, &string_store, "other.Module"); + const idx3 = try store.getOrPut(gpa, &string_store, "other.Module"); try testing.expect(idx3 != idx1); try testing.expectEqual(@as(usize, 2), store.imports.len()); // Add the first module name again - should still return original index - const idx4 = try store.getOrPut(arena_allocator, &string_store, "test.Module"); + const idx4 = try store.getOrPut(gpa, &string_store, "test.Module"); try testing.expectEqual(idx1, idx4); try testing.expectEqual(@as(usize, 2), store.imports.len()); - // Verify we can retrieve the module names through the string store - const str_idx1 = store.imports.items.items[@intFromEnum(idx1)]; - const str_idx3 = store.imports.items.items[@intFromEnum(idx3)]; - try testing.expectEqualStrings("test.Module", string_store.get(str_idx1)); - try testing.expectEqualStrings("other.Module", string_store.get(str_idx3)); + // Verify both module names are present + try testing.expect(storeContainsModule(&store, &string_store, "test.Module")); + try testing.expect(storeContainsModule(&store, &string_store, "other.Module")); } test "Import.Store empty CompactWriter roundtrip" { const testing = std.testing; const gpa = testing.allocator; - var arena = std.heap.ArenaAllocator.init(gpa); - defer arena.deinit(); - const arena_allocator = arena.allocator(); - // Create an empty Store var original = Import.Store.init(); - // No deinit needed, arena will handle it. - // Create a temp file var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -66,22 +64,19 @@ test "Import.Store empty CompactWriter roundtrip" { defer file.close(); var writer = CompactWriter.init(); - defer writer.deinit(arena_allocator); + defer writer.deinit(gpa); - const serialized = try writer.appendAlloc(arena_allocator, Import.Store.Serialized); - try serialized.serialize(&original, arena_allocator, &writer); + const serialized = try writer.appendAlloc(gpa, Import.Store.Serialized); + try serialized.serialize(&original, gpa, &writer); - // Write to file - try writer.writeGather(arena_allocator, file); + try writer.writeGather(gpa, file); - // Read back try file.seekTo(0); const buffer = try file.readToEndAlloc(gpa, 1024 * 1024); defer gpa.free(buffer); - // Cast to Serialized and deserialize const serialized_ptr = @as(*Import.Store.Serialized, @ptrCast(@alignCast(buffer.ptr))); - const deserialized = serialized_ptr.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr))), arena_allocator); + const deserialized = try serialized_ptr.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr))), gpa); // Verify empty try testing.expectEqual(@as(usize, 0), deserialized.imports.len()); @@ -91,29 +86,19 @@ test "Import.Store empty CompactWriter roundtrip" { test "Import.Store basic CompactWriter roundtrip" { const testing = std.testing; const gpa = testing.allocator; - var arena = std.heap.ArenaAllocator.init(gpa); - defer arena.deinit(); - const arena_allocator = arena.allocator(); - // Create a mock module env with string store - var string_store = try StringLiteral.Store.initCapacityBytes(arena_allocator, 1024); + var string_store = try StringLiteral.Store.initCapacityBytes(gpa, 1024); + defer string_store.deinit(gpa); - const MockEnv = struct { strings: *StringLiteral.Store }; - const mock_env = MockEnv{ .strings = &string_store }; - - // Create original store and add some imports var original = Import.Store.init(); + defer original.deinit(gpa); - const idx1 = try original.getOrPut(arena_allocator, mock_env.strings, "json.Json"); - const idx2 = try original.getOrPut(arena_allocator, mock_env.strings, "core.List"); - const idx3 = try original.getOrPut(arena_allocator, mock_env.strings, "my.Module"); + _ = try original.getOrPut(gpa, &string_store, "json.Json"); + _ = try original.getOrPut(gpa, &string_store, "core.List"); + _ = try original.getOrPut(gpa, &string_store, "my.Module"); - // Verify indices - try testing.expectEqual(@as(u32, 0), @intFromEnum(idx1)); - try testing.expectEqual(@as(u32, 1), @intFromEnum(idx2)); - try testing.expectEqual(@as(u32, 2), @intFromEnum(idx3)); + try testing.expectEqual(@as(usize, 3), original.imports.len()); - // Create a temp file var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -121,34 +106,28 @@ test "Import.Store basic CompactWriter roundtrip" { defer file.close(); var writer = CompactWriter.init(); - defer writer.deinit(arena_allocator); + defer writer.deinit(gpa); - const serialized = try writer.appendAlloc(arena_allocator, Import.Store.Serialized); - try serialized.serialize(&original, arena_allocator, &writer); + const serialized = try writer.appendAlloc(gpa, Import.Store.Serialized); + try serialized.serialize(&original, gpa, &writer); - // Write to file - try writer.writeGather(arena_allocator, file); + try writer.writeGather(gpa, file); - // Read back try file.seekTo(0); const buffer = try file.readToEndAlloc(gpa, 1024 * 1024); defer gpa.free(buffer); - // Cast to Serialized and deserialize const serialized_ptr: *Import.Store.Serialized = @ptrCast(@alignCast(buffer.ptr)); - const deserialized = serialized_ptr.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr))), arena_allocator); + var deserialized = try serialized_ptr.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr))), gpa); + defer deserialized.map.deinit(gpa); - // Verify the imports are accessible + // Verify the correct number of imports try testing.expectEqual(@as(usize, 3), deserialized.imports.len()); - // Verify the interned string IDs are stored correctly - const str_idx1 = deserialized.imports.items.items[0]; - const str_idx2 = deserialized.imports.items.items[1]; - const str_idx3 = deserialized.imports.items.items[2]; - - try testing.expectEqualStrings("json.Json", string_store.get(str_idx1)); - try testing.expectEqualStrings("core.List", string_store.get(str_idx2)); - try testing.expectEqualStrings("my.Module", string_store.get(str_idx3)); + // Verify all expected module names are present by iterating + try testing.expect(storeContainsModule(deserialized, &string_store, "json.Json")); + try testing.expect(storeContainsModule(deserialized, &string_store, "core.List")); + try testing.expect(storeContainsModule(deserialized, &string_store, "my.Module")); // Verify the map is repopulated correctly try testing.expectEqual(@as(usize, 3), deserialized.map.count()); @@ -157,28 +136,21 @@ test "Import.Store basic CompactWriter roundtrip" { test "Import.Store duplicate imports CompactWriter roundtrip" { const testing = std.testing; const gpa = testing.allocator; - var arena = std.heap.ArenaAllocator.init(gpa); - defer arena.deinit(); - const arena_allocator = arena.allocator(); - // Create a mock module env with string store - var string_store = try StringLiteral.Store.initCapacityBytes(arena_allocator, 1024); + var string_store = try StringLiteral.Store.initCapacityBytes(gpa, 1024); + defer string_store.deinit(gpa); - const MockEnv = struct { strings: *StringLiteral.Store }; - const mock_env = MockEnv{ .strings = &string_store }; - - // Create store with duplicate imports var original = Import.Store.init(); + defer original.deinit(gpa); - const idx1 = try original.getOrPut(arena_allocator, mock_env.strings, "test.Module"); - const idx2 = try original.getOrPut(arena_allocator, mock_env.strings, "another.Module"); - const idx3 = try original.getOrPut(arena_allocator, mock_env.strings, "test.Module"); // duplicate + const idx1 = try original.getOrPut(gpa, &string_store, "test.Module"); + _ = try original.getOrPut(gpa, &string_store, "another.Module"); + const idx3 = try original.getOrPut(gpa, &string_store, "test.Module"); // duplicate // Verify deduplication worked try testing.expectEqual(idx1, idx3); try testing.expectEqual(@as(usize, 2), original.imports.len()); - // Create a temp file var tmp_dir = testing.tmpDir(.{}); defer tmp_dir.cleanup(); @@ -186,42 +158,28 @@ test "Import.Store duplicate imports CompactWriter roundtrip" { defer file.close(); var writer = CompactWriter.init(); - defer writer.deinit(arena_allocator); + defer writer.deinit(gpa); - const serialized = try writer.appendAlloc(arena_allocator, Import.Store.Serialized); - try serialized.serialize(&original, arena_allocator, &writer); + const serialized = try writer.appendAlloc(gpa, Import.Store.Serialized); + try serialized.serialize(&original, gpa, &writer); - // Write to file - try writer.writeGather(arena_allocator, file); + try writer.writeGather(gpa, file); - // Read back try file.seekTo(0); const buffer = try file.readToEndAlloc(gpa, 1024 * 1024); defer gpa.free(buffer); - // Cast to Serialized and deserialize const serialized_ptr: *Import.Store.Serialized = @ptrCast(@alignCast(buffer.ptr)); - const deserialized = serialized_ptr.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr))), arena_allocator); + var deserialized = try serialized_ptr.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr))), gpa); + defer deserialized.map.deinit(gpa); - // Verify correct number of imports + // Verify correct number of imports (duplicates deduplicated) try testing.expectEqual(@as(usize, 2), deserialized.imports.len()); - // Get the string IDs and verify the strings - const str_idx1 = deserialized.imports.items.items[@intFromEnum(idx1)]; - const str_idx2 = deserialized.imports.items.items[@intFromEnum(idx2)]; - - try testing.expectEqualStrings("test.Module", string_store.get(str_idx1)); - try testing.expectEqualStrings("another.Module", string_store.get(str_idx2)); + // Verify expected module names are present + try testing.expect(storeContainsModule(deserialized, &string_store, "test.Module")); + try testing.expect(storeContainsModule(deserialized, &string_store, "another.Module")); // Verify the map was repopulated correctly try testing.expectEqual(@as(usize, 2), deserialized.map.count()); - - // Check that the map has correct entries for the string indices that were deserialized - const str_idx_0 = deserialized.imports.items.items[0]; - const str_idx_1 = deserialized.imports.items.items[1]; - - try testing.expect(deserialized.map.contains(str_idx_0)); - try testing.expect(deserialized.map.contains(str_idx_1)); - try testing.expectEqual(@as(Import.Idx, @enumFromInt(0)), deserialized.map.get(str_idx_0).?); - try testing.expectEqual(@as(Import.Idx, @enumFromInt(1)), deserialized.map.get(str_idx_1).?); } diff --git a/src/canonicalize/test/import_validation_test.zig b/src/canonicalize/test/import_validation_test.zig index 521c41396d..03d97b89e4 100644 --- a/src/canonicalize/test/import_validation_test.zig +++ b/src/canonicalize/test/import_validation_test.zig @@ -18,19 +18,25 @@ const testing = std.testing; const expectEqual = testing.expectEqual; // Helper function to parse and canonicalize source code -fn parseAndCanonicalizeSource(allocator: std.mem.Allocator, source: []const u8, module_envs: ?*std.StringHashMap(*ModuleEnv)) !struct { +fn parseAndCanonicalizeSource( + allocator: std.mem.Allocator, + source: []const u8, + module_envs: ?*std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType), +) !struct { parse_env: *ModuleEnv, ast: *parse.AST, can: *Can, } { const parse_env = try allocator.create(ModuleEnv); + // Note: We pass allocator for both gpa and arena since the ModuleEnv + // will be cleaned up by the caller parse_env.* = try ModuleEnv.init(allocator, source); const ast = try allocator.create(parse.AST); ast.* = try parse.parse(&parse_env.common, allocator); // Initialize CIR fields - try parse_env.initCIRFields(allocator, "Test"); + try parse_env.initCIRFields("Test"); const can = try allocator.create(Can); can.* = try Can.init(parse_env, ast, module_envs); @@ -46,9 +52,8 @@ test "import validation - mix of MODULE NOT FOUND, TYPE NOT EXPOSED, VALUE NOT E var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; defer std.debug.assert(gpa_state.deinit() == .ok); const allocator = gpa_state.allocator(); + // First, create some module environments with exposed items - var module_envs = std.StringHashMap(*ModuleEnv).init(allocator); - defer module_envs.deinit(); // Create module environment for "Json" module const json_env = try allocator.create(ModuleEnv); json_env.* = try ModuleEnv.init(allocator, ""); @@ -66,7 +71,7 @@ test "import validation - mix of MODULE NOT FOUND, TYPE NOT EXPOSED, VALUE NOT E try json_env.addExposedById(json_error_idx); const decode_problem_idx = try json_env.common.idents.insert(allocator, Ident.for_text("DecodeProblem")); try json_env.addExposedById(decode_problem_idx); - try module_envs.put("Json", json_env); + // Create module environment for "Utils" module const utils_env = try allocator.create(ModuleEnv); utils_env.* = try ModuleEnv.init(allocator, ""); @@ -79,9 +84,8 @@ test "import validation - mix of MODULE NOT FOUND, TYPE NOT EXPOSED, VALUE NOT E try utils_env.addExposedById(map_idx); const filter_idx = try utils_env.common.idents.insert(allocator, Ident.for_text("filter")); try utils_env.addExposedById(filter_idx); - const result_idx = try utils_env.common.idents.insert(allocator, Ident.for_text("Result")); + const result_idx = try utils_env.common.idents.insert(allocator, Ident.for_text("Try")); try utils_env.addExposedById(result_idx); - try module_envs.put("Utils", utils_env); // Parse source code with various import statements const source = \\module [main] @@ -90,7 +94,7 @@ test "import validation - mix of MODULE NOT FOUND, TYPE NOT EXPOSED, VALUE NOT E \\import Json exposing [decode, JsonError] \\ \\# Import from existing module with some invalid items - \\import Utils exposing [map, doesNotExist, Result, InvalidType] + \\import Utils exposing [map, doesNotExist, Try, InvalidType] \\ \\# Import from non-existent module \\import NonExistent exposing [something, SomeType] @@ -110,7 +114,18 @@ test "import validation - mix of MODULE NOT FOUND, TYPE NOT EXPOSED, VALUE NOT E var ast = try parse.parse(&parse_env.common, allocator); defer ast.deinit(allocator); // Initialize CIR fields - try parse_env.initCIRFields(allocator, "Test"); + try parse_env.initCIRFields("Test"); + + // Now create module_envs using parse_env's ident store + var module_envs = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(allocator); + defer module_envs.deinit(); + const json_module_ident = try parse_env.common.idents.insert(allocator, Ident.for_text("Json")); + const json_qualified_ident = try json_env.common.insertIdent(json_env.gpa, Ident.for_text("Json")); + try module_envs.put(json_module_ident, .{ .env = json_env, .qualified_type_ident = json_qualified_ident }); + const utils_module_ident = try parse_env.common.idents.insert(allocator, Ident.for_text("Utils")); + const utils_qualified_ident = try utils_env.common.insertIdent(utils_env.gpa, Ident.for_text("Utils")); + try module_envs.put(utils_module_ident, .{ .env = utils_env, .qualified_type_ident = utils_qualified_ident }); + // Canonicalize with module validation var can = try Can.init(parse_env, &ast, &module_envs); defer can.deinit(); @@ -158,13 +173,14 @@ test "import validation - mix of MODULE NOT FOUND, TYPE NOT EXPOSED, VALUE NOT E try expectEqual(true, found_does_not_exist); try expectEqual(true, found_invalid_type); // Verify that valid imports didn't generate errors - // The imports for decode, JsonError, map, Result, encode, and DecodeProblem should all work + // The imports for decode, JsonError, map, Try, encode, and DecodeProblem should all work } test "import validation - no module_envs provided" { var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; defer std.debug.assert(gpa_state.deinit() == .ok); const allocator = gpa_state.allocator(); + // Parse source code with import statements const source = \\module [main] @@ -183,7 +199,7 @@ test "import validation - no module_envs provided" { var ast = try parse.parse(&parse_env.common, allocator); defer ast.deinit(allocator); // Initialize CIR fields - try parse_env.initCIRFields(allocator, "Test"); + try parse_env.initCIRFields("Test"); // Create czer // with null module_envs var can = try Can.init(parse_env, &ast, null); @@ -196,6 +212,9 @@ test "import validation - no module_envs provided" { .module_not_found => { // expected this error message, ignore }, + .module_header_deprecated => { + // expected deprecation warning, ignore + }, else => { // these errors are not expected try testing.expect(false); @@ -235,7 +254,7 @@ test "import interner - Import.Idx functionality" { // Check that we have the correct number of unique imports (duplicates are deduplicated) // Expected: List, Dict, Json, Set (4 unique) try expectEqual(@as(usize, 4), result.parse_env.imports.imports.len()); - // Verify each unique module has an Import.Idx + // Verify each unique module has an Import.Idx by checking the imports list var found_list = false; var found_dict = false; var found_json_decode = false; @@ -257,16 +276,6 @@ test "import interner - Import.Idx functionality" { try expectEqual(true, found_dict); try expectEqual(true, found_json_decode); try expectEqual(true, found_set); - // Test the lookup functionality - // Get the Import.Idx for "List" (should be used twice) - var list_import_idx: ?CIR.Import.Idx = null; - for (result.parse_env.imports.imports.items.items, 0..) |import_string_idx, idx| { - if (std.mem.eql(u8, result.parse_env.getString(import_string_idx), "List")) { - list_import_idx = @enumFromInt(idx); - break; - } - } - try testing.expect(list_import_idx != null); } test "import interner - comprehensive usage example" { @@ -279,7 +288,7 @@ test "import interner - comprehensive usage example" { \\ \\import List exposing [map, filter] \\import Dict - \\import Result exposing [Result, withDefault] + \\import Try exposing [Try, withDefault] \\ \\process : List Str -> Dict Str Nat \\process = \items -> @@ -304,24 +313,21 @@ test "import interner - comprehensive usage example" { } _ = try result.can.canonicalizeFile(); // Check that we have the correct number of unique imports - // Expected: List, Dict, Result (3 unique) + // Expected: List, Dict, Try (3 unique) try expectEqual(@as(usize, 3), result.parse_env.imports.imports.len()); - // Verify each unique module has an Import.Idx + // Verify each unique module was imported var found_list = false; var found_dict = false; var found_result = false; - for (result.parse_env.imports.imports.items.items, 0..) |import_string_idx, idx| { - if (std.mem.eql(u8, result.parse_env.getString(import_string_idx), "List")) { + for (result.parse_env.imports.imports.items.items) |import_string_idx| { + const module_name = result.parse_env.getString(import_string_idx); + if (std.mem.eql(u8, module_name, "List")) { found_list = true; - // Note: We can't verify exposed items count here as Import.Store only stores module names - } else if (std.mem.eql(u8, result.parse_env.getString(import_string_idx), "Dict")) { + } else if (std.mem.eql(u8, module_name, "Dict")) { found_dict = true; - } else if (std.mem.eql(u8, result.parse_env.getString(import_string_idx), "Result")) { + } else if (std.mem.eql(u8, module_name, "Try")) { found_result = true; } - // Verify Import.Idx can be created from the index - const import_idx: CIR.Import.Idx = @enumFromInt(idx); - _ = import_idx; // Just verify it compiles } // Verify all expected modules were found try expectEqual(true, found_list); @@ -329,25 +335,6 @@ test "import interner - comprehensive usage example" { try expectEqual(true, found_result); } -test "Import.Idx is u32" { - - // Verify that Import.Idx is indeed a u32 enum - // Import.Idx is defined as: pub const Idx = enum(u32) { _ }; - // So we know it's backed by u32 - // Verify we can create Import.Idx values from u32 - const test_idx: u32 = 42; - const import_idx = @as(CIR.Import.Idx, @enumFromInt(test_idx)); - const back_to_u32 = @intFromEnum(import_idx); - try testing.expectEqual(test_idx, back_to_u32); - // Test that we can create valid Import.Idx values - const idx1: CIR.Import.Idx = @enumFromInt(0); - const idx2: CIR.Import.Idx = @enumFromInt(4294967295); // max u32 value - // Verify they are distinct - try testing.expect(idx1 != idx2); - // Verify the size in memory - try testing.expectEqual(@sizeOf(u32), @sizeOf(CIR.Import.Idx)); -} - test "module scopes - imports work in module scope" { var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; defer std.debug.assert(gpa_state.deinit() == .ok); @@ -417,18 +404,9 @@ test "module-qualified lookups with e_lookup_external" { allocator.destroy(result.parse_env); } _ = try result.can.canonicalizeFile(); - // Count e_lookup_external expressions - var external_lookup_count: u32 = 0; - var found_list_map = false; - var found_list_len = false; - var found_dict_insert = false; - var found_dict_empty = false; - // For this test, we're checking that module-qualified lookups work - // In the new CIR, we'd need to traverse the expression tree from the root - // For now, let's verify that the imports were registered correctly + // Verify the module names are correct const imports_list = result.parse_env.imports.imports; try testing.expect(imports_list.len() >= 2); // List and Dict - // Verify the module names are correct var has_list = false; var has_dict = false; for (imports_list.items.items) |import_string_idx| { @@ -438,28 +416,21 @@ test "module-qualified lookups with e_lookup_external" { } try testing.expect(has_list); try testing.expect(has_dict); - // TODO: Once we have proper expression traversal, verify the e_lookup_external nodes - // For now, we'll skip counting the actual lookup expressions - external_lookup_count = 4; // Expected count - found_list_map = true; - found_list_len = true; - found_dict_insert = true; - found_dict_empty = true; - // Verify we found all expected external lookups - try expectEqual(@as(u32, 4), external_lookup_count); - try expectEqual(true, found_list_map); - try expectEqual(true, found_list_len); - try expectEqual(true, found_dict_insert); - try expectEqual(true, found_dict_empty); } test "exposed_items - tracking CIR node indices for exposed items" { var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; defer std.debug.assert(gpa_state.deinit() == .ok); const allocator = gpa_state.allocator(); + // Create module environments with exposed items - var module_envs = std.StringHashMap(*ModuleEnv).init(allocator); + var module_envs = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(allocator); defer module_envs.deinit(); + + // Create temporary ident store for module name lookup + var temp_idents = try base.Ident.Store.initCapacity(allocator, 16); + defer temp_idents.deinit(allocator); + // Create a "MathUtils" module with some exposed definitions const math_env = try allocator.create(ModuleEnv); math_env.* = try ModuleEnv.init(allocator, ""); @@ -467,7 +438,7 @@ test "exposed_items - tracking CIR node indices for exposed items" { math_env.deinit(); allocator.destroy(math_env); } - // Add exposed items and set their node indices + // Add exposed items const Ident = base.Ident; const add_idx = try math_env.common.idents.insert(allocator, Ident.for_text("add")); try math_env.addExposedById(add_idx); @@ -475,12 +446,10 @@ test "exposed_items - tracking CIR node indices for exposed items" { try math_env.addExposedById(multiply_idx); const pi_idx = try math_env.common.idents.insert(allocator, Ident.for_text("PI")); try math_env.addExposedById(pi_idx); - // Simulate having CIR node indices for these exposed items - // In real usage, these would be set during canonicalization of MathUtils - try math_env.common.exposed_items.setNodeIndexById(allocator, @bitCast(add_idx), 100); - try math_env.common.exposed_items.setNodeIndexById(allocator, @bitCast(multiply_idx), 200); - try math_env.common.exposed_items.setNodeIndexById(allocator, @bitCast(pi_idx), 300); - try module_envs.put("MathUtils", math_env); + + const math_utils_ident = try temp_idents.insert(allocator, Ident.for_text("MathUtils")); + const math_utils_qualified_ident = try math_env.common.insertIdent(math_env.gpa, Ident.for_text("MathUtils")); + try module_envs.put(math_utils_ident, .{ .env = math_env, .qualified_type_ident = math_utils_qualified_ident }); // Parse source that uses these exposed items const source = \\module [calculate] @@ -504,12 +473,7 @@ test "exposed_items - tracking CIR node indices for exposed items" { allocator.destroy(result.parse_env); } _ = try result.can.canonicalizeFile(); - // Verify that e_lookup_external expressions have the correct target_node_idx values - var found_add_with_idx_100 = false; - var found_multiply_with_idx_200 = false; - var found_pi_with_idx_300 = false; - // In the new CIR, we'd need to traverse the expression tree properly - // For now, let's verify the imports were registered + // Verify the MathUtils import was registered const imports_list = result.parse_env.imports.imports; var has_mathutils = false; for (imports_list.items.items) |import_string_idx| { @@ -520,66 +484,13 @@ test "exposed_items - tracking CIR node indices for exposed items" { } } try testing.expect(has_mathutils); - // TODO: Once we have proper expression traversal, verify the target_node_idx values - // For now, we'll assume they work correctly - found_add_with_idx_100 = true; - found_multiply_with_idx_200 = true; - found_pi_with_idx_300 = true; - // Verify all lookups have the correct target node indices - try expectEqual(true, found_add_with_idx_100); - try expectEqual(true, found_multiply_with_idx_200); - try expectEqual(true, found_pi_with_idx_300); - // Test case where node index is not populated (should get 0) - const empty_env = try allocator.create(ModuleEnv); - empty_env.* = try ModuleEnv.init(allocator, ""); - defer { - empty_env.deinit(); - allocator.destroy(empty_env); - } - const undefined_idx = try empty_env.common.idents.insert(allocator, Ident.for_text("undefined")); - try empty_env.addExposedById(undefined_idx); - // Don't set node index - should default to 0 - try module_envs.put("EmptyModule", empty_env); - const source2 = - \\module [test] - \\ - \\import EmptyModule exposing [undefined] - \\ - \\test = undefined - ; - var result2 = try parseAndCanonicalizeSource(allocator, source2, &module_envs); - defer { - result2.can.deinit(); - allocator.destroy(result2.can); - result2.ast.deinit(allocator); - allocator.destroy(result2.ast); - result2.parse_env.deinit(); - allocator.destroy(result2.parse_env); - } - _ = try result2.can.canonicalizeFile(); - // Verify that undefined gets target_node_idx = 0 (not found) - var found_undefined_with_idx_0 = false; - // Verify EmptyModule was imported - const imports_list2 = result2.parse_env.imports.imports; - var has_empty_module = false; - for (imports_list2.items.items) |import_string_idx| { - const import_name = result2.parse_env.getString(import_string_idx); - if (std.mem.eql(u8, import_name, "EmptyModule")) { - has_empty_module = true; - break; - } - } - try testing.expect(has_empty_module); - // TODO: Once we have proper expression traversal, verify target_node_idx = 0 - // For now, we'll assume it works correctly - found_undefined_with_idx_0 = true; - try expectEqual(true, found_undefined_with_idx_0); } test "export count safety - ensures safe u16 casting" { var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; defer std.debug.assert(gpa_state.deinit() == .ok); const allocator = gpa_state.allocator(); + // This test verifies that we check export counts to ensure safe casting to u16 // The check triggers when exposed_items.len >= maxInt(u16) (65535) // This leaves 0 available as a potential sentinel value if needed @@ -588,7 +499,7 @@ test "export count safety - ensures safe u16 casting" { // Test the diagnostic for exactly maxInt(u16) exports var env1 = try ModuleEnv.init(allocator, ""); defer env1.deinit(); - try env1.initCIRFields(allocator, "Test"); + try env1.initCIRFields("Test"); const diag_at_limit = CIR.Diagnostic{ .too_many_exports = .{ .count = 65535, // Exactly at the limit @@ -606,7 +517,7 @@ test "export count safety - ensures safe u16 casting" { // Test the diagnostic for exceeding the limit var env2 = try ModuleEnv.init(allocator, ""); defer env2.deinit(); - try env2.initCIRFields(allocator, "Test"); + try env2.initCIRFields("Test"); const diag_over_limit = CIR.Diagnostic{ .too_many_exports = .{ .count = 70000, // Well over the limit diff --git a/src/canonicalize/test/int_test.zig b/src/canonicalize/test/int_test.zig index e8c4080b19..d370e00168 100644 --- a/src/canonicalize/test/int_test.zig +++ b/src/canonicalize/test/int_test.zig @@ -21,19 +21,13 @@ const Content = types.Content; fn getIntValue(module_env: *ModuleEnv, expr_idx: CIR.Expr.Idx) !i128 { const expr = module_env.store.getExpr(expr_idx); switch (expr) { - .e_int => |int_expr| { + .e_num => |int_expr| { return @bitCast(int_expr.value.bytes); }, else => return error.NotAnInteger, } } -fn calculateRequirements(value: i128) types.Num.Int.Requirements { - const bits_needed = types.Num.Int.BitsNeeded.fromValue(@bitCast(value)); - - return .{ .sign_needed = value < 0, .bits_needed = bits_needed }; -} - test "canonicalize simple positive integer" { const source = "42"; var test_env = try TestEnv.init(source); @@ -153,19 +147,17 @@ test "canonicalize integer with specific requirements" { const test_cases = [_]struct { source: []const u8, expected_value: i128, - expected_sign_needed: bool, - expected_bits_needed: types.Num.Int.BitsNeeded, }{ - .{ .source = "127", .expected_value = 127, .expected_sign_needed = false, .expected_bits_needed = .@"7" }, - .{ .source = "128", .expected_value = 128, .expected_sign_needed = false, .expected_bits_needed = .@"8" }, - .{ .source = "255", .expected_value = 255, .expected_sign_needed = false, .expected_bits_needed = .@"8" }, - .{ .source = "256", .expected_value = 256, .expected_sign_needed = false, .expected_bits_needed = .@"9_to_15" }, - .{ .source = "-128", .expected_value = -128, .expected_sign_needed = true, .expected_bits_needed = .@"7" }, - .{ .source = "-129", .expected_value = -129, .expected_sign_needed = true, .expected_bits_needed = .@"8" }, - .{ .source = "32767", .expected_value = 32767, .expected_sign_needed = false, .expected_bits_needed = .@"9_to_15" }, - .{ .source = "32768", .expected_value = 32768, .expected_sign_needed = false, .expected_bits_needed = .@"16" }, - .{ .source = "65535", .expected_value = 65535, .expected_sign_needed = false, .expected_bits_needed = .@"16" }, - .{ .source = "65536", .expected_value = 65536, .expected_sign_needed = false, .expected_bits_needed = .@"17_to_31" }, + .{ .source = "127", .expected_value = 127 }, + .{ .source = "128", .expected_value = 128 }, + .{ .source = "255", .expected_value = 255 }, + .{ .source = "256", .expected_value = 256 }, + .{ .source = "-128", .expected_value = -128 }, + .{ .source = "-129", .expected_value = -129 }, + .{ .source = "32767", .expected_value = 32767 }, + .{ .source = "32768", .expected_value = 32768 }, + .{ .source = "65535", .expected_value = 65535 }, + .{ .source = "65536", .expected_value = 65536 }, }; for (test_cases) |tc| { @@ -178,21 +170,6 @@ test "canonicalize integer with specific requirements" { } } -test "canonicalize integer literal creates correct type variables" { - const source = "42"; - var test_env = try TestEnv.init(source); - defer test_env.deinit(); - - const canonical_expr = try test_env.canonicalizeExpr() orelse unreachable; - const expr = test_env.getCanonicalExpr(canonical_expr.get_idx()); - switch (expr) { - .e_int => { - // Verify requirements were set - }, - else => return error.UnexpectedExprType, - } -} - test "canonicalize invalid integer literal" { // Test individual cases since some might fail during parsing vs canonicalization @@ -315,19 +292,17 @@ test "canonicalize integer requirements determination" { const test_cases = [_]struct { source: []const u8, expected_value: i128, - expected_sign_needed: bool, - expected_bits_needed: types.Num.Int.BitsNeeded, }{ // 255 needs 8 bits and no sign - .{ .source = "255", .expected_value = 255, .expected_sign_needed = false, .expected_bits_needed = .@"8" }, + .{ .source = "255", .expected_value = 255 }, // 256 needs 9-15 bits and no sign - .{ .source = "256", .expected_value = 256, .expected_sign_needed = false, .expected_bits_needed = .@"9_to_15" }, + .{ .source = "256", .expected_value = 256 }, // -1 needs sign and 7 bits - .{ .source = "-1", .expected_value = -1, .expected_sign_needed = true, .expected_bits_needed = .@"7" }, + .{ .source = "-1", .expected_value = -1 }, // 65535 needs 16 bits and no sign - .{ .source = "65535", .expected_value = 65535, .expected_sign_needed = false, .expected_bits_needed = .@"16" }, + .{ .source = "65535", .expected_value = 65535 }, // 65536 needs 17-31 bits and no sign - .{ .source = "65536", .expected_value = 65536, .expected_sign_needed = false, .expected_bits_needed = .@"17_to_31" }, + .{ .source = "65536", .expected_value = 65536 }, }; for (test_cases) |tc| { @@ -430,7 +405,7 @@ test "integer literal - negative zero" { const canonical_expr = try test_env.canonicalizeExpr() orelse unreachable; const expr = test_env.getCanonicalExpr(canonical_expr.get_idx()); switch (expr) { - .e_int => |int| { + .e_num => |int| { // -0 should be treated as 0 try testing.expectEqual(@as(i128, @bitCast(int.value.bytes)), 0); // But it should still be marked as needing a sign @@ -449,7 +424,7 @@ test "integer literal - positive zero" { const canonical_expr = try test_env.canonicalizeExpr() orelse unreachable; const expr = test_env.getCanonicalExpr(canonical_expr.get_idx()); switch (expr) { - .e_int => |int| { + .e_num => |int| { try testing.expectEqual(@as(i128, @bitCast(int.value.bytes)), 0); // Positive zero should not need a sign }, @@ -463,35 +438,33 @@ test "hexadecimal integer literals" { const test_cases = [_]struct { literal: []const u8, expected_value: i128, - expected_sign_needed: bool, - expected_bits_needed: u8, }{ // Basic hex literals - .{ .literal = "0x0", .expected_value = 0, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0x1", .expected_value = 1, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0xFF", .expected_value = 255, .expected_sign_needed = false, .expected_bits_needed = 1 }, - .{ .literal = "0x100", .expected_value = 256, .expected_sign_needed = false, .expected_bits_needed = 2 }, - .{ .literal = "0xFFFF", .expected_value = 65535, .expected_sign_needed = false, .expected_bits_needed = 3 }, - .{ .literal = "0x10000", .expected_value = 65536, .expected_sign_needed = false, .expected_bits_needed = 4 }, - .{ .literal = "0xFFFFFFFF", .expected_value = 4294967295, .expected_sign_needed = false, .expected_bits_needed = 5 }, - .{ .literal = "0x100000000", .expected_value = 4294967296, .expected_sign_needed = false, .expected_bits_needed = 6 }, - .{ .literal = "0xFFFFFFFFFFFFFFFF", .expected_value = @as(i128, @bitCast(@as(u128, 18446744073709551615))), .expected_sign_needed = false, .expected_bits_needed = 7 }, + .{ .literal = "0x0", .expected_value = 0 }, + .{ .literal = "0x1", .expected_value = 1 }, + .{ .literal = "0xFF", .expected_value = 255 }, + .{ .literal = "0x100", .expected_value = 256 }, + .{ .literal = "0xFFFF", .expected_value = 65535 }, + .{ .literal = "0x10000", .expected_value = 65536 }, + .{ .literal = "0xFFFFFFFF", .expected_value = 4294967295 }, + .{ .literal = "0x100000000", .expected_value = 4294967296 }, + .{ .literal = "0xFFFFFFFFFFFFFFFF", .expected_value = @as(i128, @bitCast(@as(u128, 18446744073709551615))) }, // Hex with underscores - .{ .literal = "0x1_000", .expected_value = 4096, .expected_sign_needed = false, .expected_bits_needed = 2 }, - .{ .literal = "0xFF_FF", .expected_value = 65535, .expected_sign_needed = false, .expected_bits_needed = 3 }, - .{ .literal = "0x1234_5678_9ABC_DEF0", .expected_value = @as(i128, @bitCast(@as(u128, 0x123456789ABCDEF0))), .expected_sign_needed = false, .expected_bits_needed = 6 }, + .{ .literal = "0x1_000", .expected_value = 4096 }, + .{ .literal = "0xFF_FF", .expected_value = 65535 }, + .{ .literal = "0x1234_5678_9ABC_DEF0", .expected_value = @as(i128, @bitCast(@as(u128, 0x123456789ABCDEF0))) }, // Negative hex literals - .{ .literal = "-0x1", .expected_value = -1, .expected_sign_needed = true, .expected_bits_needed = 0 }, - .{ .literal = "-0x80", .expected_value = -128, .expected_sign_needed = true, .expected_bits_needed = 0 }, - .{ .literal = "-0x81", .expected_value = -129, .expected_sign_needed = true, .expected_bits_needed = 1 }, - .{ .literal = "-0x8000", .expected_value = -32768, .expected_sign_needed = true, .expected_bits_needed = 2 }, - .{ .literal = "-0x8001", .expected_value = -32769, .expected_sign_needed = true, .expected_bits_needed = 3 }, - .{ .literal = "-0x80000000", .expected_value = -2147483648, .expected_sign_needed = true, .expected_bits_needed = 4 }, - .{ .literal = "-0x80000001", .expected_value = -2147483649, .expected_sign_needed = true, .expected_bits_needed = 5 }, - .{ .literal = "-0x8000000000000000", .expected_value = -9223372036854775808, .expected_sign_needed = true, .expected_bits_needed = 6 }, - .{ .literal = "-0x8000000000000001", .expected_value = @as(i128, -9223372036854775809), .expected_sign_needed = true, .expected_bits_needed = 7 }, + .{ .literal = "-0x1", .expected_value = -1 }, + .{ .literal = "-0x80", .expected_value = -128 }, + .{ .literal = "-0x81", .expected_value = -129 }, + .{ .literal = "-0x8000", .expected_value = -32768 }, + .{ .literal = "-0x8001", .expected_value = -32769 }, + .{ .literal = "-0x80000000", .expected_value = -2147483648 }, + .{ .literal = "-0x80000001", .expected_value = -2147483649 }, + .{ .literal = "-0x8000000000000000", .expected_value = -9223372036854775808 }, + .{ .literal = "-0x8000000000000001", .expected_value = @as(i128, -9223372036854775809) }, }; var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; @@ -502,7 +475,7 @@ test "hexadecimal integer literals" { var env = try ModuleEnv.init(gpa, tc.literal); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); var ast = try parse.parseExpr(&env.common, env.gpa); defer ast.deinit(gpa); @@ -518,38 +491,10 @@ test "hexadecimal integer literals" { }; const expr = env.store.getExpr(canonical_expr_idx.get_idx()); - try std.testing.expect(expr == .e_int); + try std.testing.expect(expr == .e_num); // Check the value - try std.testing.expectEqual(tc.expected_value, @as(i128, @bitCast(expr.e_int.value.bytes))); - - const expr_as_type_var: types.Var = @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx())); - const resolved = env.types.resolveVar(expr_as_type_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .num_poly => |poly| { - try std.testing.expectEqual(tc.expected_sign_needed, poly.requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, poly.requirements.bits_needed); - }, - .int_poly => |poly| { - try std.testing.expectEqual(tc.expected_sign_needed, poly.requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, poly.requirements.bits_needed); - }, - .num_unbound => |requirements| { - try std.testing.expectEqual(tc.expected_sign_needed, requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, requirements.bits_needed); - }, - .int_unbound => |requirements| { - try std.testing.expectEqual(tc.expected_sign_needed, requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, requirements.bits_needed); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - else => return error.UnexpectedContentType, - } + try std.testing.expectEqual(tc.expected_value, @as(i128, @bitCast(expr.e_num.value.bytes))); } } @@ -557,30 +502,28 @@ test "binary integer literals" { const test_cases = [_]struct { literal: []const u8, expected_value: i128, - expected_sign_needed: bool, - expected_bits_needed: u8, }{ // Basic binary literals - .{ .literal = "0b0", .expected_value = 0, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0b1", .expected_value = 1, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0b10", .expected_value = 2, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0b11111111", .expected_value = 255, .expected_sign_needed = false, .expected_bits_needed = 1 }, - .{ .literal = "0b100000000", .expected_value = 256, .expected_sign_needed = false, .expected_bits_needed = 2 }, - .{ .literal = "0b1111111111111111", .expected_value = 65535, .expected_sign_needed = false, .expected_bits_needed = 3 }, - .{ .literal = "0b10000000000000000", .expected_value = 65536, .expected_sign_needed = false, .expected_bits_needed = 4 }, + .{ .literal = "0b0", .expected_value = 0 }, + .{ .literal = "0b1", .expected_value = 1 }, + .{ .literal = "0b10", .expected_value = 2 }, + .{ .literal = "0b11111111", .expected_value = 255 }, + .{ .literal = "0b100000000", .expected_value = 256 }, + .{ .literal = "0b1111111111111111", .expected_value = 65535 }, + .{ .literal = "0b10000000000000000", .expected_value = 65536 }, // Binary with underscores - .{ .literal = "0b11_11", .expected_value = 15, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0b1111_1111", .expected_value = 255, .expected_sign_needed = false, .expected_bits_needed = 1 }, - .{ .literal = "0b1_0000_0000", .expected_value = 256, .expected_sign_needed = false, .expected_bits_needed = 2 }, - .{ .literal = "0b1010_1010_1010_1010", .expected_value = 43690, .expected_sign_needed = false, .expected_bits_needed = 3 }, + .{ .literal = "0b11_11", .expected_value = 15 }, + .{ .literal = "0b1111_1111", .expected_value = 255 }, + .{ .literal = "0b1_0000_0000", .expected_value = 256 }, + .{ .literal = "0b1010_1010_1010_1010", .expected_value = 43690 }, // Negative binary - .{ .literal = "-0b1", .expected_value = -1, .expected_sign_needed = true, .expected_bits_needed = 0 }, - .{ .literal = "-0b10000000", .expected_value = -128, .expected_sign_needed = true, .expected_bits_needed = 0 }, - .{ .literal = "-0b10000001", .expected_value = -129, .expected_sign_needed = true, .expected_bits_needed = 1 }, - .{ .literal = "-0b1000000000000000", .expected_value = -32768, .expected_sign_needed = true, .expected_bits_needed = 2 }, - .{ .literal = "-0b1000000000000001", .expected_value = -32769, .expected_sign_needed = true, .expected_bits_needed = 3 }, + .{ .literal = "-0b1", .expected_value = -1 }, + .{ .literal = "-0b10000000", .expected_value = -128 }, + .{ .literal = "-0b10000001", .expected_value = -129 }, + .{ .literal = "-0b1000000000000000", .expected_value = -32768 }, + .{ .literal = "-0b1000000000000001", .expected_value = -32769 }, }; var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; @@ -591,7 +534,7 @@ test "binary integer literals" { var env = try ModuleEnv.init(gpa, tc.literal); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); var ast = try parse.parseExpr(&env.common, env.gpa); defer ast.deinit(gpa); @@ -607,38 +550,10 @@ test "binary integer literals" { }; const expr = env.store.getExpr(canonical_expr_idx.get_idx()); - try std.testing.expect(expr == .e_int); + try std.testing.expect(expr == .e_num); // Check the value - try std.testing.expectEqual(tc.expected_value, @as(i128, @bitCast(expr.e_int.value.bytes))); - - const expr_as_type_var: types.Var = @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx())); - const resolved = env.types.resolveVar(expr_as_type_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .num_poly => |poly| { - try std.testing.expectEqual(tc.expected_sign_needed, poly.requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, poly.requirements.bits_needed); - }, - .int_poly => |poly| { - try std.testing.expectEqual(tc.expected_sign_needed, poly.requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, poly.requirements.bits_needed); - }, - .num_unbound => |requirements| { - try std.testing.expectEqual(tc.expected_sign_needed, requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, requirements.bits_needed); - }, - .int_unbound => |requirements| { - try std.testing.expectEqual(tc.expected_sign_needed, requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, requirements.bits_needed); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - else => return error.UnexpectedContentType, - } + try std.testing.expectEqual(tc.expected_value, @as(i128, @bitCast(expr.e_num.value.bytes))); } } @@ -646,30 +561,28 @@ test "octal integer literals" { const test_cases = [_]struct { literal: []const u8, expected_value: i128, - expected_sign_needed: bool, - expected_bits_needed: u8, }{ // Basic octal literals - .{ .literal = "0o0", .expected_value = 0, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0o1", .expected_value = 1, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0o7", .expected_value = 7, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0o10", .expected_value = 8, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0o377", .expected_value = 255, .expected_sign_needed = false, .expected_bits_needed = 1 }, - .{ .literal = "0o400", .expected_value = 256, .expected_sign_needed = false, .expected_bits_needed = 2 }, - .{ .literal = "0o177777", .expected_value = 65535, .expected_sign_needed = false, .expected_bits_needed = 3 }, - .{ .literal = "0o200000", .expected_value = 65536, .expected_sign_needed = false, .expected_bits_needed = 4 }, + .{ .literal = "0o0", .expected_value = 0 }, + .{ .literal = "0o1", .expected_value = 1 }, + .{ .literal = "0o7", .expected_value = 7 }, + .{ .literal = "0o10", .expected_value = 8 }, + .{ .literal = "0o377", .expected_value = 255 }, + .{ .literal = "0o400", .expected_value = 256 }, + .{ .literal = "0o177777", .expected_value = 65535 }, + .{ .literal = "0o200000", .expected_value = 65536 }, // Octal with underscores - .{ .literal = "0o377_377", .expected_value = 130815, .expected_sign_needed = false, .expected_bits_needed = 4 }, - .{ .literal = "0o1_234_567", .expected_value = 342391, .expected_sign_needed = false, .expected_bits_needed = 4 }, + .{ .literal = "0o377_377", .expected_value = 130815 }, + .{ .literal = "0o1_234_567", .expected_value = 342391 }, // Negative octal literals - .{ .literal = "-0o1", .expected_value = -1, .expected_sign_needed = true, .expected_bits_needed = 0 }, - .{ .literal = "-0o100", .expected_value = -64, .expected_sign_needed = true, .expected_bits_needed = 0 }, - .{ .literal = "-0o200", .expected_value = -128, .expected_sign_needed = true, .expected_bits_needed = 0 }, - .{ .literal = "-0o201", .expected_value = -129, .expected_sign_needed = true, .expected_bits_needed = 1 }, - .{ .literal = "-0o100000", .expected_value = -32768, .expected_sign_needed = true, .expected_bits_needed = 2 }, - .{ .literal = "-0o100001", .expected_value = -32769, .expected_sign_needed = true, .expected_bits_needed = 3 }, + .{ .literal = "-0o1", .expected_value = -1 }, + .{ .literal = "-0o100", .expected_value = -64 }, + .{ .literal = "-0o200", .expected_value = -128 }, + .{ .literal = "-0o201", .expected_value = -129 }, + .{ .literal = "-0o100000", .expected_value = -32768 }, + .{ .literal = "-0o100001", .expected_value = -32769 }, }; var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; @@ -680,7 +593,7 @@ test "octal integer literals" { var env = try ModuleEnv.init(gpa, tc.literal); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); var ast = try parse.parseExpr(&env.common, env.gpa); defer ast.deinit(gpa); @@ -696,38 +609,10 @@ test "octal integer literals" { }; const expr = env.store.getExpr(canonical_expr_idx.get_idx()); - try std.testing.expect(expr == .e_int); + try std.testing.expect(expr == .e_num); // Check the value - try std.testing.expectEqual(tc.expected_value, @as(i128, @bitCast(expr.e_int.value.bytes))); - - const expr_as_type_var: types.Var = @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx())); - const resolved = env.types.resolveVar(expr_as_type_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .num_poly => |poly| { - try std.testing.expectEqual(tc.expected_sign_needed, poly.requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, poly.requirements.bits_needed); - }, - .int_poly => |poly| { - try std.testing.expectEqual(tc.expected_sign_needed, poly.requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, poly.requirements.bits_needed); - }, - .num_unbound => |requirements| { - try std.testing.expectEqual(tc.expected_sign_needed, requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, requirements.bits_needed); - }, - .int_unbound => |requirements| { - try std.testing.expectEqual(tc.expected_sign_needed, requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, requirements.bits_needed); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - else => return error.UnexpectedContentType, - } + try std.testing.expectEqual(tc.expected_value, @as(i128, @bitCast(expr.e_num.value.bytes))); } } @@ -735,30 +620,28 @@ test "integer literals with uppercase base prefixes" { const test_cases = [_]struct { literal: []const u8, expected_value: i128, - expected_sign_needed: bool, - expected_bits_needed: u8, }{ // Uppercase hex prefix - .{ .literal = "0X0", .expected_value = 0, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0X1", .expected_value = 1, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0XFF", .expected_value = 255, .expected_sign_needed = false, .expected_bits_needed = 1 }, - .{ .literal = "0XABCD", .expected_value = 43981, .expected_sign_needed = false, .expected_bits_needed = 3 }, + .{ .literal = "0X0", .expected_value = 0 }, + .{ .literal = "0X1", .expected_value = 1 }, + .{ .literal = "0XFF", .expected_value = 255 }, + .{ .literal = "0XABCD", .expected_value = 43981 }, // Uppercase binary prefix - .{ .literal = "0B0", .expected_value = 0, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0B1", .expected_value = 1, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0B1111", .expected_value = 15, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0B11111111", .expected_value = 255, .expected_sign_needed = false, .expected_bits_needed = 1 }, + .{ .literal = "0B0", .expected_value = 0 }, + .{ .literal = "0B1", .expected_value = 1 }, + .{ .literal = "0B1111", .expected_value = 15 }, + .{ .literal = "0B11111111", .expected_value = 255 }, // Uppercase octal prefix - .{ .literal = "0O0", .expected_value = 0, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0O7", .expected_value = 7, .expected_sign_needed = false, .expected_bits_needed = 0 }, - .{ .literal = "0O377", .expected_value = 255, .expected_sign_needed = false, .expected_bits_needed = 1 }, - .{ .literal = "0O777", .expected_value = 511, .expected_sign_needed = false, .expected_bits_needed = 2 }, + .{ .literal = "0O0", .expected_value = 0 }, + .{ .literal = "0O7", .expected_value = 7 }, + .{ .literal = "0O377", .expected_value = 255 }, + .{ .literal = "0O777", .expected_value = 511 }, // Mixed case in value (should still work) - .{ .literal = "0xAbCd", .expected_value = 43981, .expected_sign_needed = false, .expected_bits_needed = 3 }, - .{ .literal = "0XaBcD", .expected_value = 43981, .expected_sign_needed = false, .expected_bits_needed = 3 }, + .{ .literal = "0xAbCd", .expected_value = 43981 }, + .{ .literal = "0XaBcD", .expected_value = 43981 }, }; var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; @@ -769,7 +652,7 @@ test "integer literals with uppercase base prefixes" { var env = try ModuleEnv.init(gpa, tc.literal); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); var ast = try parse.parseExpr(&env.common, gpa); defer ast.deinit(gpa); @@ -785,38 +668,10 @@ test "integer literals with uppercase base prefixes" { }; const expr = env.store.getExpr(canonical_expr_idx.get_idx()); - try std.testing.expect(expr == .e_int); + try std.testing.expect(expr == .e_num); // Check the value - try std.testing.expectEqual(tc.expected_value, @as(i128, @bitCast(expr.e_int.value.bytes))); - - const expr_as_type_var: types.Var = @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx())); - const resolved = env.types.resolveVar(expr_as_type_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .num_poly => |poly| { - try std.testing.expectEqual(tc.expected_sign_needed, poly.requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, poly.requirements.bits_needed); - }, - .int_poly => |poly| { - try std.testing.expectEqual(tc.expected_sign_needed, poly.requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, poly.requirements.bits_needed); - }, - .num_unbound => |requirements| { - try std.testing.expectEqual(tc.expected_sign_needed, requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, requirements.bits_needed); - }, - .int_unbound => |requirements| { - try std.testing.expectEqual(tc.expected_sign_needed, requirements.sign_needed); - try std.testing.expectEqual(tc.expected_bits_needed, requirements.bits_needed); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - else => return error.UnexpectedContentType, - } + try std.testing.expectEqual(tc.expected_value, @as(i128, @bitCast(expr.e_num.value.bytes))); } } @@ -830,43 +685,22 @@ test "numeric literal patterns use pattern idx as type var" { var env = try ModuleEnv.init(gpa, ""); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); // Create an int literal pattern directly const int_pattern = CIR.Pattern{ - .int_literal = .{ + .num_literal = .{ .value = .{ .bytes = @bitCast(@as(i128, 42)), .kind = .i128 }, + .kind = .num_unbound, }, }; - const pattern_idx = try env.addPatternAndTypeVar(int_pattern, Content{ - .structure = .{ .num = .{ .num_unbound = .{ - .sign_needed = false, - .bits_needed = 0, - } } }, - }, base.Region.zero()); + const pattern_idx = try env.addPattern(int_pattern, base.Region.zero()); // Verify the stored pattern const stored_pattern = env.store.getPattern(pattern_idx); - try std.testing.expect(stored_pattern == .int_literal); - try std.testing.expectEqual(@as(i128, 42), @as(i128, @bitCast(stored_pattern.int_literal.value.bytes))); - - // Verify the pattern index can be used as a type variable - const pattern_as_type_var: types.Var = @enumFromInt(@intFromEnum(pattern_idx)); - const resolved = env.types.resolveVar(pattern_as_type_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .num_unbound => |requirements| { - try std.testing.expectEqual(false, requirements.sign_needed); - try std.testing.expectEqual(@as(u8, 0), requirements.bits_needed); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - else => return error.UnexpectedContentType, - } + try std.testing.expect(stored_pattern == .num_literal); + try std.testing.expectEqual(@as(i128, 42), @as(i128, @bitCast(stored_pattern.num_literal.value.bytes))); } // Test that f64 literal patterns work @@ -874,308 +708,23 @@ test "numeric literal patterns use pattern idx as type var" { var env = try ModuleEnv.init(gpa, ""); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); // Create a dec literal pattern directly const dec_pattern = CIR.Pattern{ .dec_literal = .{ .value = RocDec.fromF64(3.14) orelse unreachable, + .has_suffix = false, }, }; - const pattern_idx = try env.addPatternAndTypeVar(dec_pattern, Content{ - .structure = .{ .num = .{ .frac_unbound = .{ - .fits_in_f32 = true, - .fits_in_dec = true, - } } }, - }, base.Region.zero()); + const pattern_idx = try env.addPattern(dec_pattern, base.Region.zero()); // Verify the stored pattern const stored_pattern = env.store.getPattern(pattern_idx); try std.testing.expect(stored_pattern == .dec_literal); const expected_dec = RocDec.fromF64(3.14) orelse unreachable; try std.testing.expectEqual(expected_dec.num, stored_pattern.dec_literal.value.num); - - // Verify the pattern index can be used as a type variable - const pattern_as_type_var: types.Var = @enumFromInt(@intFromEnum(pattern_idx)); - const resolved = env.types.resolveVar(pattern_as_type_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .frac_unbound => |requirements| { - try std.testing.expectEqual(true, requirements.fits_in_f32); - try std.testing.expectEqual(true, requirements.fits_in_dec); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - else => return error.UnexpectedContentType, - } - } -} - -test "numeric pattern types: unbound vs polymorphic" { - var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; - defer std.debug.assert(gpa_state.deinit() == .ok); - const gpa = gpa_state.allocator(); - - // Test int_unbound pattern - { - var env = try ModuleEnv.init(gpa, ""); - defer env.deinit(); - - try env.initCIRFields(gpa, "test"); - - const pattern = CIR.Pattern{ - .int_literal = .{ - .value = .{ .bytes = @bitCast(@as(i128, -17)), .kind = .i128 }, - }, - }; - - const pattern_idx = try env.addPatternAndTypeVar(pattern, Content{ - .structure = .{ .num = .{ .int_unbound = .{ - .sign_needed = true, - .bits_needed = 0, - } } }, - }, base.Region.zero()); - - const pattern_var: types.Var = @enumFromInt(@intFromEnum(pattern_idx)); - const resolved = env.types.resolveVar(pattern_var); - switch (resolved.desc.content) { - .structure => |s| switch (s) { - .num => |n| switch (n) { - .int_unbound => |req| { - try std.testing.expectEqual(true, req.sign_needed); - try std.testing.expectEqual(@as(u8, 0), req.bits_needed); - }, - else => return error.ExpectedIntUnbound, - }, - else => {}, - }, - else => {}, - } - } - - // Test int_poly pattern (polymorphic integer that can be different int types) - { - var env = try ModuleEnv.init(gpa, ""); - defer env.deinit(); - - try env.initCIRFields(gpa, "test"); - - const pattern = CIR.Pattern{ - .int_literal = .{ - .value = .{ .bytes = @bitCast(@as(i128, 255)), .kind = .i128 }, - }, - }; - - // Create a fresh type variable for polymorphic int - const poly_var = try env.types.fresh(); - - const pattern_idx = try env.store.addPattern(pattern, base.Region.zero()); - _ = try env.types.freshFromContent(Content{ - .structure = .{ - .num = .{ - .int_poly = .{ - .var_ = poly_var, - .requirements = .{ - .sign_needed = false, - .bits_needed = 1, // Needs at least 8 bits for 255 - }, - }, - }, - }, - }); - - const pattern_var: types.Var = @enumFromInt(@intFromEnum(pattern_idx)); - const resolved = env.types.resolveVar(pattern_var); - switch (resolved.desc.content) { - .structure => |s| switch (s) { - .num => |n| switch (n) { - .int_poly => |poly| { - try std.testing.expectEqual(false, poly.requirements.sign_needed); - try std.testing.expectEqual(@as(u8, 1), poly.requirements.bits_needed); - try std.testing.expectEqual(poly_var, poly.var_); - }, - else => return error.ExpectedIntPoly, - }, - else => {}, - }, - else => {}, - } - } - - // Test num_unbound pattern (can be int or frac) - { - var env = try ModuleEnv.init(gpa, ""); - defer env.deinit(); - - try env.initCIRFields(gpa, "test"); - - const pattern = CIR.Pattern{ - .int_literal = .{ - .value = .{ .bytes = @bitCast(@as(i128, 10)), .kind = .i128 }, - }, - }; - - const pattern_idx = try env.addPatternAndTypeVar(pattern, Content{ - .structure = .{ .num = .{ .num_unbound = .{ - .sign_needed = false, - .bits_needed = 0, - } } }, - }, base.Region.zero()); - - const pattern_var: types.Var = @enumFromInt(@intFromEnum(pattern_idx)); - const resolved = env.types.resolveVar(pattern_var); - switch (resolved.desc.content) { - .structure => |s| switch (s) { - .num => |n| switch (n) { - .num_unbound => |req| { - try std.testing.expectEqual(false, req.sign_needed); - try std.testing.expectEqual(@as(u8, 0), req.bits_needed); - }, - else => return error.ExpectedNumUnbound, - }, - else => {}, - }, - else => {}, - } - } - - // Test num_poly pattern (polymorphic num that can be int or frac) - { - var env = try ModuleEnv.init(gpa, ""); - defer env.deinit(); - - try env.initCIRFields(gpa, "test"); - - const pattern = CIR.Pattern{ - .int_literal = .{ - .value = .{ .bytes = @bitCast(@as(i128, 5)), .kind = .i128 }, - }, - }; - - // Create a fresh type variable for polymorphic num - const poly_var = try env.types.fresh(); - - const pattern_idx = try env.store.addPattern(pattern, base.Region.zero()); - _ = try env.types.freshFromContent(Content{ - .structure = .{ .num = .{ .num_poly = .{ - .var_ = poly_var, - .requirements = .{ - .sign_needed = false, - .bits_needed = 0, - }, - } } }, - }); - - const pattern_var: types.Var = @enumFromInt(@intFromEnum(pattern_idx)); - const resolved = env.types.resolveVar(pattern_var); - switch (resolved.desc.content) { - .structure => |s| switch (s) { - .num => |n| switch (n) { - .num_poly => |poly| { - try std.testing.expectEqual(false, poly.requirements.sign_needed); - try std.testing.expectEqual(@as(u8, 0), poly.requirements.bits_needed); - try std.testing.expectEqual(poly_var, poly.var_); - }, - else => return error.ExpectedNumPoly, - }, - else => {}, - }, - else => {}, - } - } - - // Test frac_unbound pattern - { - var env = try ModuleEnv.init(gpa, ""); - defer env.deinit(); - - try env.initCIRFields(gpa, "test"); - - const pattern = CIR.Pattern{ - .dec_literal = .{ - .value = RocDec.fromF64(2.5) orelse unreachable, - }, - }; - - const pattern_idx = try env.addPatternAndTypeVar(pattern, Content{ - .structure = .{ .num = .{ .frac_unbound = .{ - .fits_in_f32 = true, - .fits_in_dec = true, - } } }, - }, base.Region.zero()); - - const pattern_var: types.Var = @enumFromInt(@intFromEnum(pattern_idx)); - const resolved = env.types.resolveVar(pattern_var); - switch (resolved.desc.content) { - .structure => |s| switch (s) { - .num => |n| switch (n) { - .frac_unbound => |req| { - try std.testing.expectEqual(true, req.fits_in_f32); - try std.testing.expectEqual(true, req.fits_in_dec); - }, - else => return error.ExpectedFracUnbound, - }, - else => {}, - }, - else => {}, - } - } -} - -test "numeric pattern types: unbound vs polymorphic - frac" { - const gpa = std.testing.allocator; - - // Test frac_poly pattern - { - var env = try ModuleEnv.init(gpa, ""); - defer env.deinit(); - - try env.initCIRFields(gpa, "test"); - - const pattern = CIR.Pattern{ - .dec_literal = .{ - .value = RocDec.fromF64(1000000.0) orelse unreachable, - }, - }; - - // Create a fresh type variable for polymorphic frac - const poly_var = try env.types.fresh(); - - const pattern_idx = try env.store.addPattern(pattern, base.Region.zero()); - _ = try env.types.freshFromContent(Content{ - .structure = .{ - .num = .{ - .frac_poly = .{ - .var_ = poly_var, - .requirements = .{ - .fits_in_f32 = true, - .fits_in_dec = false, // Infinity doesn't fit in Dec - }, - }, - }, - }, - }); - - const pattern_var: types.Var = @enumFromInt(@intFromEnum(pattern_idx)); - const resolved = env.types.resolveVar(pattern_var); - switch (resolved.desc.content) { - .structure => |s| switch (s) { - .num => |n| switch (n) { - .frac_poly => |poly| { - try std.testing.expectEqual(true, poly.requirements.fits_in_f32); - try std.testing.expectEqual(false, poly.requirements.fits_in_dec); - try std.testing.expectEqual(poly_var, poly.var_); - }, - else => return error.ExpectedFracPoly, - }, - else => {}, - }, - else => {}, - } } } @@ -1189,27 +738,29 @@ test "pattern numeric literal value edge cases" { var env = try ModuleEnv.init(gpa, ""); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); // Test i128 max const max_pattern = CIR.Pattern{ - .int_literal = .{ + .num_literal = .{ .value = .{ .bytes = @bitCast(@as(i128, std.math.maxInt(i128))), .kind = .i128 }, + .kind = .num_unbound, }, }; const max_idx = try env.store.addPattern(max_pattern, base.Region.zero()); const stored_max = env.store.getPattern(max_idx); - try std.testing.expectEqual(std.math.maxInt(i128), @as(i128, @bitCast(stored_max.int_literal.value.bytes))); + try std.testing.expectEqual(std.math.maxInt(i128), @as(i128, @bitCast(stored_max.num_literal.value.bytes))); // Test i128 min const min_pattern = CIR.Pattern{ - .int_literal = .{ + .num_literal = .{ .value = .{ .bytes = @bitCast(@as(i128, std.math.minInt(i128))), .kind = .i128 }, + .kind = .num_unbound, }, }; const min_idx = try env.store.addPattern(min_pattern, base.Region.zero()); const stored_min = env.store.getPattern(min_idx); - try std.testing.expectEqual(std.math.minInt(i128), @as(i128, @bitCast(stored_min.int_literal.value.bytes))); + try std.testing.expectEqual(std.math.minInt(i128), @as(i128, @bitCast(stored_min.num_literal.value.bytes))); } // Test small decimal pattern @@ -1217,13 +768,15 @@ test "pattern numeric literal value edge cases" { var env = try ModuleEnv.init(gpa, ""); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); const small_dec_pattern = CIR.Pattern{ .small_dec_literal = .{ - .numerator = 1234, - .denominator_power_of_ten = 2, // 12.34 - + .value = .{ + .numerator = 1234, + .denominator_power_of_ten = 2, // 12.34 + }, + .has_suffix = false, }, }; @@ -1231,8 +784,8 @@ test "pattern numeric literal value edge cases" { const stored = env.store.getPattern(pattern_idx); try std.testing.expect(stored == .small_dec_literal); - try std.testing.expectEqual(@as(i16, 1234), stored.small_dec_literal.numerator); - try std.testing.expectEqual(@as(u8, 2), stored.small_dec_literal.denominator_power_of_ten); + try std.testing.expectEqual(@as(i16, 1234), stored.small_dec_literal.value.numerator); + try std.testing.expectEqual(@as(u8, 2), stored.small_dec_literal.value.denominator_power_of_ten); } // Test dec literal pattern @@ -1240,12 +793,12 @@ test "pattern numeric literal value edge cases" { var env = try ModuleEnv.init(gpa, ""); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); const dec_pattern = CIR.Pattern{ .dec_literal = .{ .value = RocDec{ .num = 314159265358979323 }, // π * 10^17 - + .has_suffix = false, }, }; @@ -1261,12 +814,13 @@ test "pattern numeric literal value edge cases" { var env = try ModuleEnv.init(gpa, ""); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); // Test negative zero (RocDec doesn't distinguish between +0 and -0) const neg_zero_pattern = CIR.Pattern{ .dec_literal = .{ .value = RocDec.fromF64(-0.0) orelse unreachable, + .has_suffix = false, }, }; const neg_zero_idx = try env.store.addPattern(neg_zero_pattern, base.Region.zero()); @@ -1276,213 +830,6 @@ test "pattern numeric literal value edge cases" { } } -test "pattern literal type transitions" { - var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; - defer std.debug.assert(gpa_state.deinit() == .ok); - const gpa = gpa_state.allocator(); - - // Test transitioning from unbound to concrete type - { - var env = try ModuleEnv.init(gpa, ""); - defer env.deinit(); - - try env.initCIRFields(gpa, "test"); - - const pattern = CIR.Pattern{ - .int_literal = .{ - .value = .{ .bytes = @bitCast(@as(i128, 100)), .kind = .i128 }, - }, - }; - - const pattern_idx = try env.addPatternAndTypeVar(pattern, Content{ - .structure = .{ .num = .{ .num_unbound = .{ - .sign_needed = false, - .bits_needed = 1, - } } }, - }, base.Region.zero()); - - // Simulate type inference determining it's a U8 - const pattern_var: types.Var = @enumFromInt(@intFromEnum(pattern_idx)); - _ = try env.types.setVarContent(pattern_var, Content{ - .structure = .{ .num = types.Num.int_u8 }, - }); - - // Verify it resolved to U8 - const resolved = env.types.resolveVar(pattern_var); - switch (resolved.desc.content) { - .structure => |s| switch (s) { - .num => |n| switch (n) { - .num_compact => |compact| switch (compact) { - .int => |int| { - try std.testing.expect(int == .u8); - }, - else => return error.ExpectedInt, - }, - else => return error.ExpectedNumCompact, - }, - else => {}, - }, - else => {}, - } - } - - // Test hex/binary/octal patterns must be integers - { - var env = try ModuleEnv.init(gpa, ""); - defer env.deinit(); - - try env.initCIRFields(gpa, "test"); - - // Hex pattern (0xFF) - const hex_pattern = CIR.Pattern{ - .int_literal = .{ - .value = .{ .bytes = @bitCast(@as(i128, 0xFF)), .kind = .i128 }, - }, - }; - - const hex_idx = try env.addPatternAndTypeVar(hex_pattern, Content{ - .structure = .{ .num = .{ .int_unbound = .{ - .sign_needed = false, - .bits_needed = 1, - } } }, - }, base.Region.zero()); - - const hex_var: types.Var = @enumFromInt(@intFromEnum(hex_idx)); - const resolved = env.types.resolveVar(hex_var); - switch (resolved.desc.content) { - .structure => |s| switch (s) { - .num => |n| switch (n) { - .int_unbound => |req| { - // Verify it's constrained to integers only - try std.testing.expectEqual(false, req.sign_needed); - try std.testing.expectEqual(@as(u8, 1), req.bits_needed); - }, - else => return error.ExpectedIntUnbound, - }, - else => {}, - }, - else => {}, - } - } -} - -test "pattern type inference with numeric literals" { - var gpa_state = std.heap.GeneralPurposeAllocator(.{ .safety = true }){}; - defer std.debug.assert(gpa_state.deinit() == .ok); - const gpa = gpa_state.allocator(); - - // Test that pattern indices work correctly as type variables with type inference - { - var env = try ModuleEnv.init(gpa, ""); - defer env.deinit(); - - try env.initCIRFields(gpa, "test"); - - // Create patterns representing different numeric literals - const patterns = [_]struct { - pattern: CIR.Pattern, - expected_type: types.Content, - }{ - // Small positive int - could be any unsigned type - .{ - .pattern = CIR.Pattern{ - .int_literal = .{ - .value = .{ .bytes = @bitCast(@as(i128, 42)), .kind = .i128 }, - }, - }, - .expected_type = Content{ .structure = .{ .num = .{ .num_unbound = .{ - .sign_needed = false, - .bits_needed = 0, - } } } }, - }, - // Negative int - needs signed type - .{ - .pattern = CIR.Pattern{ - .int_literal = .{ - .value = .{ .bytes = @bitCast(@as(i128, -42)), .kind = .i128 }, - }, - }, - .expected_type = Content{ .structure = .{ .num = .{ .num_unbound = .{ - .sign_needed = true, - .bits_needed = 0, - } } } }, - }, - // Large int requiring more bits - .{ - .pattern = CIR.Pattern{ - .int_literal = .{ - .value = .{ .bytes = @bitCast(@as(i128, 65536)), .kind = .i128 }, - }, - }, - .expected_type = Content{ - .structure = .{ - .num = .{ - .num_unbound = .{ - .sign_needed = false, - .bits_needed = 4, // Needs at least 17 bits - }, - }, - }, - }, - }, - }; - - for (patterns) |test_case| { - const pattern_idx = try env.addPatternAndTypeVar(test_case.pattern, test_case.expected_type, base.Region.zero()); - - // Verify the pattern index works as a type variable - const pattern_var: types.Var = @enumFromInt(@intFromEnum(pattern_idx)); - const resolved = env.types.resolveVar(pattern_var); - - // Compare the resolved type with expected - try std.testing.expectEqual(test_case.expected_type, resolved.desc.content); - } - } - - // Test patterns with type constraints from context - { - var env = try ModuleEnv.init(gpa, ""); - defer env.deinit(); - - try env.initCIRFields(gpa, "test"); - - // Create a pattern that will be constrained by context - const pattern = CIR.Pattern{ - .int_literal = .{ - .value = .{ .bytes = @bitCast(@as(i128, 100)), .kind = .i128 }, - }, - }; - - const pattern_idx = try env.addPatternAndTypeVar(pattern, Content{ - .structure = .{ .num = .{ .num_unbound = .{ - .sign_needed = false, - .bits_needed = 1, - } } }, - }, base.Region.zero()); - - // Simulate type inference constraining it to U8 - const pattern_var: types.Var = @enumFromInt(@intFromEnum(pattern_idx)); - _ = try env.types.setVarContent(pattern_var, Content{ - .structure = .{ .num = types.Num.int_u8 }, - }); - - // Verify it was constrained correctly - const resolved = env.types.resolveVar(pattern_var); - switch (resolved.desc.content) { - .structure => |s| switch (s) { - .num => |n| switch (n) { - .num_compact => |compact| { - try std.testing.expect(compact.int == .u8); - }, - else => return error.ExpectedConcreteType, - }, - else => {}, - }, - else => {}, - } - } -} - test "parseIntWithUnderscores function" { // Test the parseIntWithUnderscores helper function directly const test_cases = [_]struct { @@ -1544,8 +891,8 @@ test "parseIntWithUnderscores function" { } } -test "parseNumLiteralWithSuffix function" { - // Test the parseNumLiteralWithSuffix function to ensure correct parsing of prefixes and suffixes +test "parseNumeralWithSuffix function" { + // Test the parseNumeralWithSuffix function to ensure correct parsing of prefixes and suffixes const test_cases = [_]struct { input: []const u8, expected_num_text: []const u8, @@ -1583,10 +930,10 @@ test "parseNumLiteralWithSuffix function" { }; for (test_cases) |tc| { - const result = types.Num.parseNumLiteralWithSuffix(tc.input); + const result = types.parseNumeralWithSuffix(tc.input); if (!std.mem.eql(u8, result.num_text, tc.expected_num_text)) { - std.debug.print("MISMATCH num_text: parseNumLiteralWithSuffix('{s}').num_text = '{s}' (expected '{s}')\n", .{ tc.input, result.num_text, tc.expected_num_text }); + std.debug.print("MISMATCH num_text: parseNumeralWithSuffix('{s}').num_text = '{s}' (expected '{s}')\n", .{ tc.input, result.num_text, tc.expected_num_text }); } try std.testing.expectEqualSlices(u8, tc.expected_num_text, result.num_text); @@ -1617,7 +964,7 @@ test "hex literal parsing logic integration" { for (test_cases) |tc| { // Mimic the exact parsing logic from canonicalizeExpr - const parsed = types.Num.parseNumLiteralWithSuffix(tc.literal); + const parsed = types.parseNumeralWithSuffix(tc.literal); const is_negated = parsed.num_text[0] == '-'; const after_minus_sign = @as(usize, @intFromBool(is_negated)); @@ -1670,3 +1017,329 @@ test "hex literal parsing logic integration" { try std.testing.expectEqual(tc.expected_value, u128_val); } } + +// number req tests // + +test "IntValue.toIntRequirements - boundary values for each type" { + // u8 boundary: 255/256 + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .u128 }; + const test_val: u128 = 255; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(!req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"8".toBits()); + } + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .u128 }; + const test_val: u128 = 256; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(!req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"9_to_15".toBits()); + } + + // i8 positive boundary: 127/128 + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = 127; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(!req.sign_needed); // Positive doesn't need sign + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"7".toBits()); + } + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = 128; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(!req.sign_needed); // Positive doesn't need sign + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"8".toBits()); + } + + // i8 negative boundary: -127/-128/-129 + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = -127; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"7".toBits()); + } + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = -128; // Special case! + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"7".toBits()); // Due to special case + } + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = -129; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"8".toBits()); + } + + // u16 boundary: 65535/65536 + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .u128 }; + const test_val: u128 = 65535; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(!req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"16".toBits()); + } + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .u128 }; + const test_val: u128 = 65536; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(!req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"17_to_31".toBits()); + } + + // i16 boundaries: 32767/-32768/-32769 + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = 32767; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(!req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"9_to_15".toBits()); + } + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = -32768; // Special case! + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"9_to_15".toBits()); // Due to special case + } + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = -32769; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"16".toBits()); + } +} + +test "IntValue.toIntRequirements - zero and small values" { + // Zero (special case: doesn't need sign even if stored as signed) + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const req = val.toIntRequirements(); + try testing.expect(!req.sign_needed); // Zero doesn't need sign + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"7".toBits()); + } + + // 1 and -1 + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = 1; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(!req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"7".toBits()); + } + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = -1; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"7".toBits()); + } +} + +test "IntValue.toIntRequirements - powers of 2 edge cases" { + // Powers of 2 that are NOT minimum signed values + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = -256; // Power of 2, but not a minimum signed value + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"9_to_15".toBits()); // Should NOT be special cased + } + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = -512; // Power of 2, but not a minimum signed value + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"9_to_15".toBits()); // Should NOT be special cased + } +} + +test "IntValue.toIntRequirements - i32 boundaries" { + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = 2147483647; // i32 max + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(!req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"17_to_31".toBits()); + } + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = -2147483648; // i32 min (special case!) + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"17_to_31".toBits()); // Due to special case + } + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = -2147483649; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"32".toBits()); + } +} + +test "IntValue.toIntRequirements - i64 boundaries" { + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = std.math.maxInt(i64); // 9223372036854775807 + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(!req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"33_to_63".toBits()); + } + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = std.math.minInt(i64); // -9223372036854775808 (special case!) + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"33_to_63".toBits()); // Due to special case + } +} + +test "IntValue.toIntRequirements - u128 max" { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .u128 }; + const test_val: u128 = std.math.maxInt(u128); + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toIntRequirements(); + try testing.expect(!req.sign_needed); + try testing.expectEqual(req.bits_needed, types.Int.BitsNeeded.@"128".toBits()); +} + +test "IntValue.toFracRequirements - f32 precision boundaries" { + // 2^24 - 1 (largest consecutive integer in f32) + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .u128 }; + const test_val: u128 = 16777215; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toFracRequirements(); + try testing.expect(req.fits_in_f32); + } + + // 2^24 (still representable exactly) + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .u128 }; + const test_val: u128 = 16777216; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toFracRequirements(); + try testing.expect(req.fits_in_f32); + } + + // 2^24 + 1 (first integer that can't be exactly represented) + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .u128 }; + const test_val: u128 = 16777217; + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toFracRequirements(); + try testing.expect(!req.fits_in_f32); // Cannot be exactly represented + } +} + +test "IntValue.toFracRequirements - Dec boundaries" { + // Value near Dec's positive limit (~1.7e20) + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = 170141183460469231731; // Just under Dec's limit + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toFracRequirements(); + try testing.expect(req.fits_in_dec); + } + + // Value exceeding Dec's positive limit + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = std.math.maxInt(i128); // Way over Dec's limit + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toFracRequirements(); + try testing.expect(!req.fits_in_dec); + } + + // Value near Dec's negative limit + { + var val = CIR.IntValue{ .bytes = [_]u8{0} ** 16, .kind = .i128 }; + const test_val: i128 = -170141183460469231731; // Just within Dec's limit + @memcpy(val.bytes[0..16], std.mem.asBytes(&test_val)); + const req = val.toFracRequirements(); + try testing.expect(req.fits_in_dec); + } +} + +test "SmallDecValue edge cases" { + // Maximum denominator power (produces very small but non-zero value) + { + const val = CIR.SmallDecValue{ .numerator = 1, .denominator_power_of_ten = 255 }; + const f64_val = val.toF64(); + // This doesn't underflow to 0 - f64 can represent very small values + try testing.expect(f64_val > 0.0); + try testing.expect(f64_val < 1e-250); // Very small + const req = val.toFracRequirements(); + // Very small values don't fit in f32 (would underflow) + try testing.expect(!req.fits_in_f32); // Too small for f32 + try testing.expect(req.fits_in_dec); // But fits in Dec (as 0 or very small) + } + + // Large numerator with large denominator (should produce normal value) + { + const val = CIR.SmallDecValue{ .numerator = 32767, .denominator_power_of_ten = 4 }; + const f64_val = val.toF64(); + try testing.expectApproxEqAbs(@as(f64, 3.2767), f64_val, 0.0001); + const req = val.toFracRequirements(); + try testing.expect(req.fits_in_f32); + try testing.expect(req.fits_in_dec); + } + + // Negative max numerator + { + const val = CIR.SmallDecValue{ .numerator = -32768, .denominator_power_of_ten = 4 }; + const f64_val = val.toF64(); + try testing.expectApproxEqAbs(@as(f64, -3.2768), f64_val, 0.0001); + const req = val.toFracRequirements(); + try testing.expect(req.fits_in_f32); + try testing.expect(req.fits_in_dec); + } + + // Value that would be subnormal in f32 (but still representable) + { + const val = CIR.SmallDecValue{ .numerator = 1, .denominator_power_of_ten = 40 }; + const f64_val = val.toF64(); + try testing.expectEqual(@as(f64, 1e-40), f64_val); + const req = val.toFracRequirements(); + try testing.expect(req.fits_in_f32); // CAN be represented as subnormal in f32 + try testing.expect(req.fits_in_dec); + } + + // Value that's too small even for f32 subnormals + { + const val = CIR.SmallDecValue{ .numerator = 1, .denominator_power_of_ten = 46 }; + const f64_val = val.toF64(); + try testing.expectEqual(@as(f64, 1e-46), f64_val); + const req = val.toFracRequirements(); + try testing.expect(!req.fits_in_f32); // Below f32's true minimum (~1.4e-45) + try testing.expect(req.fits_in_dec); + } +} diff --git a/src/canonicalize/test/node_store_test.zig b/src/canonicalize/test/node_store_test.zig index a34d57cb54..8b3e8e5152 100644 --- a/src/canonicalize/test/node_store_test.zig +++ b/src/canonicalize/test/node_store_test.zig @@ -8,6 +8,7 @@ const builtins = @import("builtins"); const StringLiteral = base.StringLiteral; const CIR = @import("../CIR.zig"); +const ModuleEnv = @import("../ModuleEnv.zig"); const NodeStore = @import("../NodeStore.zig"); const RocDec = builtins.dec.RocDec; const CalledVia = base.CalledVia; @@ -54,13 +55,14 @@ fn rand_region() base.Region { test "NodeStore round trip - Statements" { const gpa = testing.allocator; + var store = try NodeStore.init(gpa); defer store.deinit(); - var statements = std.ArrayList(CIR.Statement).init(gpa); - defer statements.deinit(); + var statements = std.ArrayList(CIR.Statement).empty; + defer statements.deinit(gpa); - try statements.append(CIR.Statement{ + try statements.append(gpa, CIR.Statement{ .s_decl = .{ .pattern = rand_idx(CIR.Pattern.Idx), .expr = rand_idx(CIR.Expr.Idx), @@ -68,45 +70,53 @@ test "NodeStore round trip - Statements" { }, }); - try statements.append(CIR.Statement{ + try statements.append(gpa, CIR.Statement{ + .s_decl_gen = .{ + .pattern = rand_idx(CIR.Pattern.Idx), + .expr = rand_idx(CIR.Expr.Idx), + .anno = rand_idx(CIR.Annotation.Idx), + }, + }); + + try statements.append(gpa, CIR.Statement{ .s_var = .{ .pattern_idx = rand_idx(CIR.Pattern.Idx), .expr = rand_idx(CIR.Expr.Idx), }, }); - try statements.append(CIR.Statement{ + try statements.append(gpa, CIR.Statement{ .s_reassign = .{ .pattern_idx = rand_idx(CIR.Pattern.Idx), .expr = rand_idx(CIR.Expr.Idx), }, }); - try statements.append(CIR.Statement{ + try statements.append(gpa, CIR.Statement{ .s_expr = .{ .expr = rand_idx(CIR.Expr.Idx), }, }); - try statements.append(CIR.Statement{ + try statements.append(gpa, CIR.Statement{ .s_crash = .{ .msg = rand_idx(StringLiteral.Idx), }, }); - try statements.append(CIR.Statement{ + try statements.append(gpa, CIR.Statement{ .s_dbg = .{ .expr = rand_idx(CIR.Expr.Idx), }, }); - try statements.append(CIR.Statement{ + try statements.append(gpa, CIR.Statement{ .s_expect = .{ .body = rand_idx(CIR.Expr.Idx), }, }); - try statements.append(CIR.Statement{ + try statements.append(gpa, CIR.Statement{ .s_for = .{ .patt = rand_idx(CIR.Pattern.Idx), .expr = rand_idx(CIR.Expr.Idx), @@ -114,13 +124,21 @@ test "NodeStore round trip - Statements" { }, }); - try statements.append(CIR.Statement{ - .s_return = .{ - .expr = rand_idx(CIR.Expr.Idx), + try statements.append(gpa, CIR.Statement{ + .s_while = .{ + .cond = rand_idx(CIR.Expr.Idx), + .body = rand_idx(CIR.Expr.Idx), }, }); - try statements.append(CIR.Statement{ + try statements.append(gpa, CIR.Statement{ + .s_return = .{ + .expr = rand_idx(CIR.Expr.Idx), + .lambda = rand_idx(CIR.Expr.Idx), + }, + }); + + try statements.append(gpa, CIR.Statement{ .s_import = .{ .module_name_tok = rand_ident_idx(), .qualifier_tok = rand_ident_idx(), @@ -129,27 +147,34 @@ test "NodeStore round trip - Statements" { }, }); - try statements.append(CIR.Statement{ + try statements.append(gpa, CIR.Statement{ .s_alias_decl = .{ .header = rand_idx(CIR.TypeHeader.Idx), .anno = rand_idx(CIR.TypeAnno.Idx), }, }); - try statements.append(CIR.Statement{ + try statements.append(gpa, CIR.Statement{ .s_nominal_decl = .{ .header = rand_idx(CIR.TypeHeader.Idx), .anno = rand_idx(CIR.TypeAnno.Idx), + .is_opaque = false, }, }); - try statements.append(CIR.Statement{ .s_type_anno = .{ + try statements.append(gpa, CIR.Statement{ .s_type_anno = .{ .name = rand_ident_idx(), .anno = rand_idx(CIR.TypeAnno.Idx), .where = null, } }); - try statements.append(CIR.Statement{ .s_runtime_error = .{ + try statements.append(gpa, CIR.Statement{ .s_type_var_alias = .{ + .alias_name = rand_ident_idx(), + .type_var_name = rand_ident_idx(), + .type_var_anno = rand_idx(CIR.TypeAnno.Idx), + } }); + + try statements.append(gpa, CIR.Statement{ .s_runtime_error = .{ .diagnostic = rand_idx(CIR.Diagnostic.Idx), } }); @@ -158,132 +183,137 @@ test "NodeStore round trip - Statements" { const idx = try store.addStatement(stmt, region); const retrieved = store.getStatement(idx); - testing.expectEqualDeep(stmt, retrieved) catch |err| { - std.debug.print("\n\nOriginal: {any}\n\n", .{stmt}); - std.debug.print("Retrieved: {any}\n\n", .{retrieved}); - return err; - }; + try testing.expectEqualDeep(stmt, retrieved); } const actual_test_count = statements.items.len; if (actual_test_count < NodeStore.MODULEENV_STATEMENT_NODE_COUNT) { - std.debug.print("Statement test coverage insufficient! Need at least {d} test cases but found {d}.\n", .{ NodeStore.MODULEENV_STATEMENT_NODE_COUNT, actual_test_count }); - std.debug.print("Please add test cases for missing statement variants.\n", .{}); return error.IncompleteStatementTestCoverage; } } test "NodeStore round trip - Expressions" { const gpa = testing.allocator; + var store = try NodeStore.init(gpa); defer store.deinit(); - var expressions = std.ArrayList(CIR.Expr).init(gpa); - defer expressions.deinit(); + var expressions = std.ArrayList(CIR.Expr).empty; + defer expressions.deinit(gpa); - try expressions.append(CIR.Expr{ - .e_int = .{ + try expressions.append(gpa, CIR.Expr{ + .e_num = .{ .value = .{ .bytes = @bitCast(@as(i128, 42)), .kind = .i128 }, + .kind = .i128, }, }); - try expressions.append(CIR.Expr{ - .e_frac_f32 = .{ .value = rand.random().float(f32) }, + try expressions.append(gpa, CIR.Expr{ + .e_frac_f32 = .{ .value = rand.random().float(f32), .has_suffix = false }, }); - try expressions.append(CIR.Expr{ - .e_frac_f64 = .{ .value = rand.random().float(f64) }, + try expressions.append(gpa, CIR.Expr{ + .e_frac_f64 = .{ .value = rand.random().float(f64), .has_suffix = false }, }); - try expressions.append(CIR.Expr{ - .e_frac_dec = .{ + try expressions.append(gpa, CIR.Expr{ + .e_dec = .{ .value = RocDec{ .num = 314 }, + .has_suffix = false, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_dec_small = .{ - .numerator = rand.random().int(i16), - .denominator_power_of_ten = rand.random().int(u8), + .value = .{ + .numerator = rand.random().int(i16), + .denominator_power_of_ten = rand.random().int(u8), + }, + .has_suffix = false, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_str_segment = .{ .literal = rand_idx(StringLiteral.Idx), }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_str = .{ .span = CIR.Expr.Span{ .span = rand_span() }, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_lookup_local = .{ .pattern_idx = rand_idx(CIR.Pattern.Idx), }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_lookup_external = .{ .module_idx = rand_idx_u16(CIR.Import.Idx), .target_node_idx = rand.random().int(u16), .region = rand_region(), }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ + .e_lookup_required = .{ + .requires_idx = ModuleEnv.RequiredType.SafeList.Idx.fromU32(rand.random().int(u32)), + }, + }); + try expressions.append(gpa, CIR.Expr{ .e_list = .{ - .elem_var = rand_idx(TypeVar), .elems = CIR.Expr.Span{ .span = rand_span() }, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_tuple = .{ .elems = CIR.Expr.Span{ .span = rand_span() }, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_match = CIR.Expr.Match{ .cond = rand_idx(CIR.Expr.Idx), .branches = CIR.Expr.Match.Branch.Span{ .span = rand_span() }, .exhaustive = rand_idx(TypeVar), }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_if = .{ .branches = CIR.Expr.IfBranch.Span{ .span = rand_span() }, .final_else = rand_idx(CIR.Expr.Idx), }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_call = .{ + .func = rand_idx(CIR.Expr.Idx), .args = CIR.Expr.Span{ .span = rand_span() }, .called_via = CalledVia.apply, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_record = .{ .fields = CIR.RecordField.Span{ .span = rand_span() }, .ext = null, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_empty_list = .{}, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_block = .{ .stmts = CIR.Statement.Span{ .span = rand_span() }, .final_expr = rand_idx(CIR.Expr.Idx), }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_tag = .{ .name = rand_ident_idx(), .args = CIR.Expr.Span{ .span = rand_span() }, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_nominal = .{ .nominal_type_decl = rand_idx(CIR.Statement.Idx), .backing_expr = rand_idx(CIR.Expr.Idx), .backing_type = .tag, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_zero_argument_tag = .{ .closure_name = rand_ident_idx(), .variant_var = rand_idx(TypeVar), @@ -291,67 +321,69 @@ test "NodeStore round trip - Expressions" { .name = rand_ident_idx(), }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_closure = .{ .lambda_idx = rand_idx(CIR.Expr.Idx), .captures = CIR.Expr.Capture.Span{ .span = rand_span() }, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_lambda = .{ .args = CIR.Pattern.Span{ .span = rand_span() }, .body = rand_idx(CIR.Expr.Idx), }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_binop = CIR.Expr.Binop.init( .add, rand_idx(CIR.Expr.Idx), rand_idx(CIR.Expr.Idx), ), }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_unary_minus = CIR.Expr.UnaryMinus.init(rand_idx(CIR.Expr.Idx)), }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_unary_not = CIR.Expr.UnaryNot.init(rand_idx(CIR.Expr.Idx)), }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_dot_access = .{ .receiver = rand_idx(CIR.Expr.Idx), .field_name = rand_ident_idx(), + .field_name_region = rand_region(), .args = null, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_runtime_error = .{ .diagnostic = rand_idx(CIR.Diagnostic.Idx), }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_crash = .{ .msg = rand_idx(StringLiteral.Idx), }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_dbg = .{ .expr = rand_idx(CIR.Expr.Idx), }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_empty_record = .{}, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_expect = .{ .body = rand_idx(CIR.Expr.Idx), }, }); - try expressions.append(CIR.Expr{ - .e_frac_dec = .{ + try expressions.append(gpa, CIR.Expr{ + .e_dec = .{ .value = RocDec{ .num = 123456789 }, + .has_suffix = false, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_nominal_external = .{ .module_idx = rand_idx_u16(CIR.Import.Idx), .target_node_idx = rand.random().int(u16), @@ -359,140 +391,178 @@ test "NodeStore round trip - Expressions" { .backing_type = .tag, }, }); - try expressions.append(CIR.Expr{ + try expressions.append(gpa, CIR.Expr{ .e_ellipsis = .{}, }); + try expressions.append(gpa, CIR.Expr{ + .e_anno_only = .{}, + }); + try expressions.append(gpa, CIR.Expr{ + .e_hosted_lambda = .{ + .symbol_name = rand_ident_idx(), + .index = rand.random().int(u32), + .args = CIR.Pattern.Span{ .span = rand_span() }, + .body = rand_idx(CIR.Expr.Idx), + }, + }); + try expressions.append(gpa, CIR.Expr{ + .e_return = .{ + .expr = rand_idx(CIR.Expr.Idx), + }, + }); + try expressions.append(gpa, CIR.Expr{ + .e_for = .{ + .patt = rand_idx(CIR.Pattern.Idx), + .expr = rand_idx(CIR.Expr.Idx), + .body = rand_idx(CIR.Expr.Idx), + }, + }); + try expressions.append(gpa, CIR.Expr{ + .e_type_var_dispatch = .{ + .type_var_alias_stmt = rand_idx(CIR.Statement.Idx), + .method_name = rand_ident_idx(), + .args = .{ .span = .{ .start = rand.random().int(u32), .len = rand.random().int(u32) } }, + }, + }); for (expressions.items, 0..) |expr, i| { const region = from_raw_offsets(@intCast(i * 100), @intCast(i * 100 + 50)); const idx = try store.addExpr(expr, region); const retrieved = store.getExpr(idx); - testing.expectEqualDeep(expr, retrieved) catch |err| { - std.debug.print("\n\nOriginal: {any}\n\n", .{expr}); - std.debug.print("Retrieved: {any}\n\n", .{retrieved}); - return err; - }; + try testing.expectEqualDeep(expr, retrieved); } const actual_test_count = expressions.items.len; if (actual_test_count < NodeStore.MODULEENV_EXPR_NODE_COUNT) { - std.debug.print("Expression test coverage insufficient! Need at least {d} test cases but found {d}.\n", .{ NodeStore.MODULEENV_EXPR_NODE_COUNT, actual_test_count }); - std.debug.print("Please add test cases for missing expression variants.\n", .{}); return error.IncompleteExpressionTestCoverage; } } test "NodeStore round trip - Diagnostics" { const gpa = testing.allocator; + var store = try NodeStore.init(gpa); defer store.deinit(); - var diagnostics = std.ArrayList(CIR.Diagnostic).init(gpa); - defer diagnostics.deinit(); + var diagnostics = std.ArrayList(CIR.Diagnostic).empty; + defer diagnostics.deinit(gpa); // Test all diagnostic types to ensure complete coverage - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .not_implemented = .{ .feature = rand_idx(StringLiteral.Idx), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .invalid_num_literal = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .ident_already_in_scope = .{ .ident = rand_ident_idx(), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .crash_expects_string = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .ident_not_in_scope = .{ .ident = rand_ident_idx(), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ + .qualified_ident_does_not_exist = .{ + .ident = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ .invalid_top_level_statement = .{ .stmt = rand_idx(StringLiteral.Idx), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .expr_not_canonicalized = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .invalid_string_interpolation = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .pattern_arg_invalid = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .pattern_not_canonicalized = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .can_lambda_not_implemented = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .lambda_body_not_canonicalized = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .if_condition_not_canonicalized = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .if_then_not_canonicalized = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .if_else_not_canonicalized = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ + .if_expr_without_else = .{ + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ .var_across_function_boundary = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .shadowing_warning = .{ .ident = rand_ident_idx(), .region = rand_region(), @@ -500,7 +570,7 @@ test "NodeStore round trip - Diagnostics" { }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .type_redeclared = .{ .name = rand_ident_idx(), .redeclared_region = rand_region(), @@ -508,60 +578,124 @@ test "NodeStore round trip - Diagnostics" { }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .undeclared_type = .{ .name = rand_ident_idx(), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .undeclared_type_var = .{ .name = rand_ident_idx(), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .type_alias_but_needed_nominal = .{ .name = rand_ident_idx(), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .malformed_type_annotation = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .malformed_where_clause = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .where_clause_not_allowed_in_type_decl = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ + .type_module_missing_matching_type = .{ + .module_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ + .default_app_missing_main = .{ + .module_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ + .default_app_wrong_arity = .{ + .arity = 2, + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ + .cannot_import_default_app = .{ + .module_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ + .execution_requires_app_or_default_app = .{ + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ + .type_name_case_mismatch = .{ + .module_name = rand_ident_idx(), + .type_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ + .module_header_deprecated = .{ + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ + .redundant_expose_main_type = .{ + .type_name = rand_ident_idx(), + .module_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ + .invalid_main_type_rename_in_exposing = .{ + .type_name = rand_ident_idx(), + .alias = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ .unused_variable = .{ .ident = rand_ident_idx(), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .used_underscore_variable = .{ .ident = rand_ident_idx(), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .type_alias_redeclared = .{ .name = rand_ident_idx(), .original_region = rand_region(), @@ -569,7 +703,7 @@ test "NodeStore round trip - Diagnostics" { }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .nominal_type_redeclared = .{ .name = rand_ident_idx(), .original_region = rand_region(), @@ -577,7 +711,7 @@ test "NodeStore round trip - Diagnostics" { }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .type_shadowed_warning = .{ .name = rand_ident_idx(), .region = rand_region(), @@ -586,7 +720,7 @@ test "NodeStore round trip - Diagnostics" { }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .type_parameter_conflict = .{ .name = rand_ident_idx(), .parameter_name = rand_ident_idx(), @@ -595,7 +729,7 @@ test "NodeStore round trip - Diagnostics" { }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .duplicate_record_field = .{ .field_name = rand_ident_idx(), .duplicate_region = rand_region(), @@ -603,19 +737,13 @@ test "NodeStore round trip - Diagnostics" { }, }); - try diagnostics.append(CIR.Diagnostic{ - .invalid_single_quote = .{ - .region = rand_region(), - }, - }); - - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .f64_pattern_literal = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .unused_type_var_name = .{ .name = rand_ident_idx(), .suggested_name = rand_ident_idx(), @@ -623,7 +751,7 @@ test "NodeStore round trip - Diagnostics" { }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .type_var_marked_unused = .{ .name = rand_ident_idx(), .suggested_name = rand_ident_idx(), @@ -631,41 +759,41 @@ test "NodeStore round trip - Diagnostics" { }, }); - try diagnostics.append(CIR.Diagnostic{ - .type_var_ending_in_underscore = .{ + try diagnostics.append(gpa, CIR.Diagnostic{ + .type_var_starting_with_dollar = .{ .name = rand_ident_idx(), .suggested_name = rand_ident_idx(), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .underscore_in_type_declaration = .{ .is_alias = rand.random().boolean(), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .tuple_elem_not_canonicalized = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .empty_tuple = .{ .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .exposed_but_not_implemented = .{ .ident = rand_ident_idx(), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .redundant_exposed = .{ .ident = rand_ident_idx(), .region = rand_region(), @@ -673,14 +801,14 @@ test "NodeStore round trip - Diagnostics" { }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .module_not_found = .{ .module_name = rand_ident_idx(), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .value_not_exposed = .{ .module_name = rand_ident_idx(), .value_name = rand_ident_idx(), @@ -688,7 +816,7 @@ test "NodeStore round trip - Diagnostics" { }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .type_not_exposed = .{ .module_name = rand_ident_idx(), .type_name = rand_ident_idx(), @@ -696,98 +824,158 @@ test "NodeStore round trip - Diagnostics" { }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ .module_not_imported = .{ .module_name = rand_ident_idx(), .region = rand_region(), }, }); - try diagnostics.append(CIR.Diagnostic{ + try diagnostics.append(gpa, CIR.Diagnostic{ + .nested_type_not_found = .{ + .parent_name = rand_ident_idx(), + .nested_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ + .nested_value_not_found = .{ + .parent_name = rand_ident_idx(), + .nested_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(gpa, CIR.Diagnostic{ .too_many_exports = .{ .count = rand.random().int(u32), .region = rand_region(), }, }); + try diagnostics.append(gpa, CIR.Diagnostic{ + .type_from_missing_module = .{ + .module_name = rand_ident_idx(), + .type_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + // Test the round-trip for all diagnostics for (diagnostics.items) |diagnostic| { const idx = try store.addDiagnostic(diagnostic); const retrieved = store.getDiagnostic(idx); - testing.expectEqualDeep(diagnostic, retrieved) catch |err| { - std.debug.print("\n\nOriginal: {any}\n\n", .{diagnostic}); - std.debug.print("Retrieved: {any}\n\n", .{retrieved}); - return err; - }; + try testing.expectEqualDeep(diagnostic, retrieved); } const actual_test_count = diagnostics.items.len; if (actual_test_count < NodeStore.MODULEENV_DIAGNOSTIC_NODE_COUNT) { - std.debug.print("Diagnostic test coverage insufficient! Need at least {d} test cases but found {d}.\n", .{ NodeStore.MODULEENV_DIAGNOSTIC_NODE_COUNT, actual_test_count }); - std.debug.print("Please add test cases for missing diagnostic variants.\n", .{}); return error.IncompleteDiagnosticTestCoverage; } } test "NodeStore round trip - TypeAnno" { const gpa = testing.allocator; + var store = try NodeStore.init(gpa); defer store.deinit(); - var type_annos = std.ArrayList(CIR.TypeAnno).init(gpa); - defer type_annos.deinit(); + var type_annos = std.ArrayList(CIR.TypeAnno).empty; + defer type_annos.deinit(gpa); // Test all TypeAnno variants to ensure complete coverage - try type_annos.append(CIR.TypeAnno{ + try type_annos.append(gpa, CIR.TypeAnno{ .apply = .{ - .symbol = rand_ident_idx(), + .name = rand_ident_idx(), + .base = .{ .builtin = .dec }, + .args = CIR.TypeAnno.Span{ .span = rand_span() }, + }, + }); + try type_annos.append(gpa, CIR.TypeAnno{ + .apply = .{ + .name = rand_ident_idx(), + .base = .{ .local = .{ .decl_idx = @enumFromInt(10) } }, + .args = CIR.TypeAnno.Span{ .span = rand_span() }, + }, + }); + try type_annos.append(gpa, CIR.TypeAnno{ + .apply = .{ + .name = rand_ident_idx(), + .base = .{ .external = .{ + .module_idx = rand_idx(CIR.Import.Idx), + .target_node_idx = rand.random().int(u16), + } }, .args = CIR.TypeAnno.Span{ .span = rand_span() }, }, }); - try type_annos.append(CIR.TypeAnno{ - .ty_var = .{ + try type_annos.append(gpa, CIR.TypeAnno{ + .rigid_var = .{ .name = rand_ident_idx(), }, }); + try type_annos.append(gpa, CIR.TypeAnno{ + .rigid_var_lookup = .{ + .ref = rand_idx(CIR.TypeAnno.Idx), + }, + }); - try type_annos.append(CIR.TypeAnno{ + try type_annos.append(gpa, CIR.TypeAnno{ .underscore = {}, }); - try type_annos.append(CIR.TypeAnno{ - .ty = .{ - .symbol = rand_ident_idx(), + try type_annos.append(gpa, CIR.TypeAnno{ + .lookup = .{ + .name = rand_ident_idx(), + .base = .{ .builtin = .dec }, + }, + }); + try type_annos.append(gpa, CIR.TypeAnno{ + .lookup = .{ + .name = rand_ident_idx(), + .base = .{ .local = .{ .decl_idx = @enumFromInt(10) } }, + }, + }); + try type_annos.append(gpa, CIR.TypeAnno{ + .lookup = .{ + .name = rand_ident_idx(), + .base = .{ .external = .{ + .module_idx = rand_idx(CIR.Import.Idx), + .target_node_idx = rand.random().int(u16), + } }, }, }); - try type_annos.append(CIR.TypeAnno{ - .ty = .{ - .symbol = rand_ident_idx(), - }, - }); - - try type_annos.append(CIR.TypeAnno{ + try type_annos.append(gpa, CIR.TypeAnno{ .tag_union = .{ .tags = CIR.TypeAnno.Span{ .span = rand_span() }, .ext = rand_idx(CIR.TypeAnno.Idx), }, }); - try type_annos.append(CIR.TypeAnno{ + try type_annos.append(gpa, CIR.TypeAnno{ + .tag = .{ + .name = rand_ident_idx(), + .args = CIR.TypeAnno.Span{ .span = rand_span() }, + }, + }); + + try type_annos.append(gpa, CIR.TypeAnno{ .tuple = .{ .elems = CIR.TypeAnno.Span{ .span = rand_span() }, }, }); - try type_annos.append(CIR.TypeAnno{ + try type_annos.append(gpa, CIR.TypeAnno{ .record = .{ .fields = CIR.TypeAnno.RecordField.Span{ .span = rand_span() }, + .ext = null, }, }); - try type_annos.append(CIR.TypeAnno{ + try type_annos.append(gpa, CIR.TypeAnno{ .@"fn" = .{ .args = CIR.TypeAnno.Span{ .span = rand_span() }, .ret = rand_idx(CIR.TypeAnno.Idx), @@ -795,26 +983,13 @@ test "NodeStore round trip - TypeAnno" { }, }); - try type_annos.append(CIR.TypeAnno{ + try type_annos.append(gpa, CIR.TypeAnno{ .parens = .{ .anno = rand_idx(CIR.TypeAnno.Idx), }, }); - try type_annos.append(CIR.TypeAnno{ - .ty = .{ - .symbol = rand_ident_idx(), - }, - }); - - try type_annos.append(CIR.TypeAnno{ - .ty_lookup_external = .{ - .module_idx = rand_idx(CIR.Import.Idx), - .target_node_idx = rand.random().int(u16), - }, - }); - - try type_annos.append(CIR.TypeAnno{ + try type_annos.append(gpa, CIR.TypeAnno{ .malformed = .{ .diagnostic = rand_idx(CIR.Diagnostic.Idx), }, @@ -826,55 +1001,55 @@ test "NodeStore round trip - TypeAnno" { const idx = try store.addTypeAnno(type_anno, region); const retrieved = store.getTypeAnno(idx); - testing.expectEqualDeep(type_anno, retrieved) catch |err| { - std.debug.print("\n\nOriginal: {any}\n\n", .{type_anno}); - std.debug.print("Retrieved: {any}\n\n", .{retrieved}); - return err; - }; + try testing.expectEqualDeep(type_anno, retrieved); } + // We have extra tests for: + // * apply -> 2 + // * lookup -> 2 + const extra_test_count = 4; + const actual_test_count = type_annos.items.len; - if (actual_test_count < NodeStore.MODULEENV_TYPE_ANNO_NODE_COUNT) { - std.debug.print("CIR.TypeAnno test coverage insufficient! Need at least {d} test cases but found {d}.\n", .{ NodeStore.MODULEENV_TYPE_ANNO_NODE_COUNT, actual_test_count }); - std.debug.print("Please add test cases for missing type annotation variants.\n", .{}); + if (actual_test_count - extra_test_count < NodeStore.MODULEENV_TYPE_ANNO_NODE_COUNT) { return error.IncompleteTypeAnnoTestCoverage; } } test "NodeStore round trip - Pattern" { const gpa = testing.allocator; + var store = try NodeStore.init(gpa); defer store.deinit(); - var patterns = std.ArrayList(CIR.Pattern).init(gpa); - defer patterns.deinit(); + var patterns = std.ArrayList(CIR.Pattern).empty; + defer patterns.deinit(gpa); // Test all Pattern variants to ensure complete coverage - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .assign = .{ .ident = rand_ident_idx(), }, }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .as = .{ .pattern = rand_idx(CIR.Pattern.Idx), .ident = rand_ident_idx(), }, }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .applied_tag = .{ .name = rand_ident_idx(), .args = CIR.Pattern.Span{ .span = rand_span() }, }, }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .nominal = .{ .nominal_type_decl = rand_idx(CIR.Statement.Idx), .backing_pattern = rand_idx(CIR.Pattern.Idx), .backing_type = .tag, }, }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .nominal_external = .{ .module_idx = rand_idx_u16(CIR.Import.Idx), .target_node_idx = rand.random().int(u16), @@ -882,62 +1057,63 @@ test "NodeStore round trip - Pattern" { .backing_type = .tag, }, }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .record_destructure = .{ - .whole_var = rand_idx(TypeVar), - .ext_var = rand_idx(TypeVar), .destructs = CIR.Pattern.RecordDestruct.Span{ .span = rand_span() }, }, }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .list = .{ - .list_var = rand_idx(TypeVar), - .elem_var = rand_idx(TypeVar), .patterns = CIR.Pattern.Span{ .span = rand_span() }, .rest_info = .{ .index = rand.random().int(u32), .pattern = rand_idx(CIR.Pattern.Idx) }, }, }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .tuple = .{ .patterns = CIR.Pattern.Span{ .span = rand_span() }, }, }); - try patterns.append(CIR.Pattern{ - .int_literal = .{ + try patterns.append(gpa, CIR.Pattern{ + .num_literal = .{ .value = CIR.IntValue{ .bytes = @bitCast(rand.random().int(i128)), .kind = .i128, }, + .kind = .int_unbound, }, }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .small_dec_literal = .{ - .numerator = rand.random().int(i16), - .denominator_power_of_ten = rand.random().int(u8), + .value = .{ + .numerator = rand.random().int(i16), + .denominator_power_of_ten = rand.random().int(u8), + }, + .has_suffix = true, }, }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .dec_literal = .{ .value = RocDec.fromU64(rand.random().int(u64)), + .has_suffix = false, }, }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .str_literal = .{ .literal = rand_idx(StringLiteral.Idx), }, }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .frac_f32_literal = .{ .value = rand.random().float(f32), }, }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .frac_f64_literal = .{ .value = rand.random().float(f64), }, }); - try patterns.append(CIR.Pattern{ .underscore = {} }); - try patterns.append(CIR.Pattern{ + try patterns.append(gpa, CIR.Pattern{ .underscore = {} }); + try patterns.append(gpa, CIR.Pattern{ .runtime_error = .{ .diagnostic = rand_idx(CIR.Diagnostic.Idx), }, @@ -953,25 +1129,15 @@ test "NodeStore round trip - Pattern" { const idx = try store.addPattern(pattern, region); const retrieved = store.getPattern(idx); - testing.expectEqualDeep(pattern, retrieved) catch |err| { - std.debug.print("\n\nOriginal: {any}\n\n", .{pattern}); - std.debug.print("Retrieved: {any}\n\n", .{retrieved}); - return err; - }; + try testing.expectEqualDeep(pattern, retrieved); // Also verify the region was stored correctly const stored_region = store.getRegionAt(@enumFromInt(@intFromEnum(idx))); - testing.expectEqualDeep(region, stored_region) catch |err| { - std.debug.print("\n\nExpected region: {any}\n\n", .{region}); - std.debug.print("Stored region: {any}\n\n", .{stored_region}); - return err; - }; + try testing.expectEqualDeep(region, stored_region); } const actual_test_count = patterns.items.len; if (actual_test_count < NodeStore.MODULEENV_PATTERN_NODE_COUNT) { - std.debug.print("CIR.Pattern test coverage insufficient! Need at least {d} test cases but found {d}.\n", .{ NodeStore.MODULEENV_PATTERN_NODE_COUNT, actual_test_count }); - std.debug.print("Please add test cases for missing pattern variants.\n", .{}); return error.IncompletePatternTestCoverage; } } diff --git a/src/canonicalize/test/record_test.zig b/src/canonicalize/test/record_test.zig index 87ee4cd896..b1b0634150 100644 --- a/src/canonicalize/test/record_test.zig +++ b/src/canonicalize/test/record_test.zig @@ -21,7 +21,7 @@ test "record literal uses record_unbound" { var env = try ModuleEnv.init(gpa, source); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); var ast = try parse.parseExpr(&env.common, gpa); defer ast.deinit(gpa); @@ -34,20 +34,14 @@ test "record literal uses record_unbound" { return error.CanonicalizeError; }; - // Get the type of the expression - const expr_var = @as(TypeVar, @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx()))); - const resolved = env.types.resolveVar(expr_var); - - // Check that it's a record_unbound - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .record_unbound => |fields| { - // Success! The record literal created a record_unbound type - try std.testing.expect(fields.len() == 2); - }, - else => return error.ExpectedRecordUnbound, + const canonical_expr = env.store.getExpr(canonical_expr_idx.idx); + // Check that it's a record + switch (canonical_expr) { + .e_record => |record| { + // Success! The record literal created a record + try std.testing.expect(record.fields.span.len == 2); }, - else => return error.ExpectedStructure, + else => return error.ExpectedRecord, } } @@ -58,7 +52,7 @@ test "record literal uses record_unbound" { var env = try ModuleEnv.init(gpa, source2); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); var ast = try parse.parseExpr(&env.common, gpa); defer ast.deinit(gpa); @@ -71,19 +65,13 @@ test "record literal uses record_unbound" { return error.CanonicalizeError; }; - // Get the type of the expression - const expr_var = @as(TypeVar, @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx()))); - const resolved = env.types.resolveVar(expr_var); - + const canonical_expr = env.store.getExpr(canonical_expr_idx.idx); // Check that it's an empty_record - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .empty_record => { - // Success! Empty record literal created empty_record type - }, - else => return error.ExpectedEmptyRecord, + switch (canonical_expr) { + .e_empty_record => { + // Success! Empty record literal created empty_record }, - else => return error.ExpectedStructure, + else => return error.ExpectedEmptyRecord, } } @@ -95,7 +83,7 @@ test "record literal uses record_unbound" { var env = try ModuleEnv.init(gpa, source3); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); var ast = try parse.parseExpr(&env.common, gpa); defer ast.deinit(gpa); @@ -108,38 +96,35 @@ test "record literal uses record_unbound" { return error.CanonicalizeError; }; - // Get the type of the expression - const expr_var = @as(TypeVar, @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx()))); - const resolved = env.types.resolveVar(expr_var); + const canonical_expr = env.store.getExpr(canonical_expr_idx.idx); + // Check that it's a record + switch (canonical_expr) { + .e_record => |record| { + // Success! The record literal created a record + try std.testing.expect(record.fields.span.len == 1); - // Check that it's a record_unbound - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .record_unbound => |fields| { - // Success! The record literal created a record_unbound type - try std.testing.expect(fields.len() == 1); + const cir_fields = env.store.sliceRecordFields(record.fields); - // Check the field - const fields_slice = env.types.getRecordFieldsSlice(fields); - const field_name = env.getIdent(fields_slice.get(0).name); - try std.testing.expectEqualStrings("value", field_name); - }, - else => return error.ExpectedRecordUnbound, + const cir_field = env.store.getRecordField(cir_fields[0]); + + const field_name = env.getIdent(cir_field.name); + try std.testing.expectEqualStrings("value", field_name); }, - else => return error.ExpectedStructure, + else => return error.ExpectedRecord, } } } test "record_unbound basic functionality" { const gpa = std.testing.allocator; + const source = "{ x: 42, y: 99 }"; // Test that record literals create record_unbound types var env = try ModuleEnv.init(gpa, source); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); var ast = try parse.parseExpr(&env.common, gpa); defer ast.deinit(gpa); @@ -152,36 +137,35 @@ test "record_unbound basic functionality" { return error.CanonicalizeError; }; - // Get the type of the expression - const expr_var = @as(TypeVar, @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx()))); - const resolved = env.types.resolveVar(expr_var); + const canonical_expr = env.store.getExpr(canonical_expr_idx.idx); + // Check that it's a record + switch (canonical_expr) { + .e_record => |record| { + // Success! The record literal created a record + try std.testing.expect(record.fields.span.len == 2); - // Verify it starts as record_unbound - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .record_unbound => |fields| { - // Success! Record literal created record_unbound type - try std.testing.expect(fields.len() == 2); + const cir_fields = env.store.sliceRecordFields(record.fields); - // Check field names - const field_slice = env.types.getRecordFieldsSlice(fields); - try std.testing.expectEqualStrings("x", env.getIdent(field_slice.get(0).name)); - try std.testing.expectEqualStrings("y", env.getIdent(field_slice.get(1).name)); - }, - else => return error.ExpectedRecordUnbound, + const cir_field_0 = env.store.getRecordField(cir_fields[0]); + const cir_field_1 = env.store.getRecordField(cir_fields[1]); + + // Check field names + try std.testing.expectEqualStrings("x", env.getIdent(cir_field_0.name)); + try std.testing.expectEqualStrings("y", env.getIdent(cir_field_1.name)); }, - else => return error.ExpectedStructure, + else => return error.ExpectedRecord, } } test "record_unbound with multiple fields" { const gpa = std.testing.allocator; + const source = "{ a: 123, b: 456, c: 789 }"; var env = try ModuleEnv.init(gpa, source); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); // Create record_unbound with multiple fields var ast = try parse.parseExpr(&env.common, gpa); @@ -195,96 +179,180 @@ test "record_unbound with multiple fields" { return error.CanonicalizeError; }; - const expr_var = @as(TypeVar, @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx()))); - const resolved = env.types.resolveVar(expr_var); + const canonical_expr = env.store.getExpr(canonical_expr_idx.idx); + // Check that it's a record + switch (canonical_expr) { + .e_record => |record| { + // Success! The record literal created a record + try std.testing.expect(record.fields.span.len == 3); - // Should be record_unbound - switch (resolved.desc.content) { - .structure => |s| switch (s) { - .record_unbound => |fields| { - try std.testing.expect(fields.len() == 3); + const cir_fields = env.store.sliceRecordFields(record.fields); - // Check field names - const field_slice = env.types.getRecordFieldsSlice(fields); - try std.testing.expectEqualStrings("a", env.getIdent(field_slice.get(0).name)); - try std.testing.expectEqualStrings("b", env.getIdent(field_slice.get(1).name)); - try std.testing.expectEqualStrings("c", env.getIdent(field_slice.get(2).name)); - }, - else => return error.ExpectedRecordUnbound, + const cir_field_0 = env.store.getRecordField(cir_fields[0]); + const cir_field_1 = env.store.getRecordField(cir_fields[1]); + const cir_field_2 = env.store.getRecordField(cir_fields[2]); + + // Check field names + try std.testing.expectEqualStrings("a", env.getIdent(cir_field_0.name)); + try std.testing.expectEqualStrings("b", env.getIdent(cir_field_1.name)); + try std.testing.expectEqualStrings("c", env.getIdent(cir_field_2.name)); }, - else => return error.ExpectedStructure, + else => return error.ExpectedRecord, } } -test "record with extension variable" { +const CIR = @import("../CIR.zig"); +const Pattern = CIR.Pattern; + +test "record pattern destructuring" { const gpa = std.testing.allocator; - var env = try ModuleEnv.init(gpa, ""); + // Test simple record destructuring: { x, y } = { x: 1, y: 2 } + const source = "{ x, y } = { x: 1, y: 2 }"; + + var env = try ModuleEnv.init(gpa, source); defer env.deinit(); - try env.initCIRFields(gpa, "test"); + try env.initCIRFields("test"); - // Test that regular records have extension variables - // Create { x: 42, y: "hi" }* (open record) - const num_var = try env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }); - const str_var = try env.types.freshFromContent(Content{ .structure = .str }); + var ast = try parse.parseStatement(&env.common, gpa); + defer ast.deinit(gpa); - const fields = [_]types.RecordField{ - .{ .name = try env.insertIdent(Ident.for_text("x")), .var_ = num_var }, - .{ .name = try env.insertIdent(Ident.for_text("y")), .var_ = str_var }, - }; - const fields_range = try env.types.appendRecordFields(&fields); - const ext_var = try env.types.fresh(); // Open extension - const record_content = Content{ .structure = .{ .record = .{ .fields = fields_range, .ext = ext_var } } }; - const record_var = try env.types.freshFromContent(record_content); + var can = try Can.init(&env, &ast, null); + defer can.deinit(); - // Verify the record has an extension variable - const resolved = env.types.resolveVar(record_var); - switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .record => |record| { - try std.testing.expect(record.fields.len() == 2); + // Enter a function scope so we can have local bindings + try can.scopeEnter(gpa, true); - // Check that extension is a flex var (open record) - const ext_resolved = env.types.resolveVar(record.ext); - switch (ext_resolved.desc.content) { - .flex_var => { - // Success! The record has an open extension - }, - else => return error.ExpectedFlexVar, - } - }, - else => return error.ExpectedRecord, + const stmt_idx: parse.AST.Statement.Idx = @enumFromInt(ast.root_node_idx); + const stmt = ast.store.getStatement(stmt_idx); + + // The statement should be a declaration + switch (stmt) { + .decl => |decl| { + // Get the pattern from the declaration + const pattern_idx = decl.pattern; + const canonical_pattern_idx = try can.canonicalizePattern(pattern_idx) orelse { + return error.CanonicalizePatternError; + }; + + const canonical_pattern = env.store.getPattern(canonical_pattern_idx); + + // Check that it's a record_destructure pattern + switch (canonical_pattern) { + .record_destructure => |rd| { + // Get the destructs + const destructs = env.store.sliceRecordDestructs(rd.destructs); + try std.testing.expect(destructs.len == 2); + + // Check the first destruct (x) + const destruct_x = env.store.getRecordDestruct(destructs[0]); + try std.testing.expectEqualStrings("x", env.getIdent(destruct_x.label)); + + // Check the second destruct (y) + const destruct_y = env.store.getRecordDestruct(destructs[1]); + try std.testing.expectEqualStrings("y", env.getIdent(destruct_y.label)); + + // Verify that x and y are now in scope + const x_ident = try env.insertIdent(Ident.for_text("x")); + const y_ident = try env.insertIdent(Ident.for_text("y")); + + const x_lookup = can.scopeLookup(.ident, x_ident); + const y_lookup = can.scopeLookup(.ident, y_ident); + + // Both should be found in scope + switch (x_lookup) { + .found => {}, + else => return error.XNotInScope, + } + switch (y_lookup) { + .found => {}, + else => return error.YNotInScope, + } + }, + else => return error.ExpectedRecordDestructure, + } }, - else => return error.ExpectedStructure, - } - - // Now test a closed record - const closed_ext_var = try env.types.freshFromContent(Content{ .structure = .empty_record }); - const closed_record_content = Content{ .structure = .{ .record = .{ .fields = fields_range, .ext = closed_ext_var } } }; - const closed_record_var = try env.types.freshFromContent(closed_record_content); - - // Verify the closed record has empty_record as extension - const closed_resolved = env.types.resolveVar(closed_record_var); - switch (closed_resolved.desc.content) { - .structure => |structure| switch (structure) { - .record => |record| { - try std.testing.expect(record.fields.len() == 2); - - // Check that extension is empty_record (closed record) - const ext_resolved = env.types.resolveVar(record.ext); - switch (ext_resolved.desc.content) { - .structure => |ext_structure| switch (ext_structure) { - .empty_record => { - // Success! The record is closed - }, - else => return error.ExpectedEmptyRecord, - }, - else => return error.ExpectedStructure, - } - }, - else => return error.ExpectedRecord, - }, - else => return error.ExpectedStructure, + else => return error.ExpectedDecl, + } +} + +test "record pattern with sub-patterns" { + const gpa = std.testing.allocator; + + // Test record destructuring with sub-patterns: { name: n, age: a } = person + const source = "{ name: n, age: a } = person"; + + var env = try ModuleEnv.init(gpa, source); + defer env.deinit(); + + try env.initCIRFields("test"); + + var ast = try parse.parseStatement(&env.common, gpa); + defer ast.deinit(gpa); + + var can = try Can.init(&env, &ast, null); + defer can.deinit(); + + // Enter a function scope so we can have local bindings + try can.scopeEnter(gpa, true); + + const stmt_idx: parse.AST.Statement.Idx = @enumFromInt(ast.root_node_idx); + const stmt = ast.store.getStatement(stmt_idx); + + // The statement should be a declaration + switch (stmt) { + .decl => |decl| { + // Get the pattern from the declaration + const pattern_idx = decl.pattern; + const canonical_pattern_idx = try can.canonicalizePattern(pattern_idx) orelse { + return error.CanonicalizePatternError; + }; + + const canonical_pattern = env.store.getPattern(canonical_pattern_idx); + + // Check that it's a record_destructure pattern + switch (canonical_pattern) { + .record_destructure => |rd| { + // Get the destructs + const destructs = env.store.sliceRecordDestructs(rd.destructs); + try std.testing.expect(destructs.len == 2); + + // Check the first destruct (name: n) + const destruct_name = env.store.getRecordDestruct(destructs[0]); + try std.testing.expectEqualStrings("name", env.getIdent(destruct_name.label)); + // The ident should be the sub-pattern variable name + try std.testing.expectEqualStrings("name", env.getIdent(destruct_name.ident)); + // Should have a SubPattern kind + switch (destruct_name.kind) { + .SubPattern => {}, + else => return error.ExpectedSubPattern, + } + + // Check the second destruct (age: a) + const destruct_age = env.store.getRecordDestruct(destructs[1]); + try std.testing.expectEqualStrings("age", env.getIdent(destruct_age.label)); + + // Verify that n and a are now in scope (the sub-pattern bindings) + const n_ident = try env.insertIdent(Ident.for_text("n")); + const a_ident = try env.insertIdent(Ident.for_text("a")); + + const n_lookup = can.scopeLookup(.ident, n_ident); + const a_lookup = can.scopeLookup(.ident, a_ident); + + // Both should be found in scope + switch (n_lookup) { + .found => {}, + else => return error.NNotInScope, + } + switch (a_lookup) { + .found => {}, + else => return error.ANotInScope, + } + }, + else => return error.ExpectedRecordDestructure, + } + }, + else => return error.ExpectedDecl, } } diff --git a/src/canonicalize/test/roc_emitter_test.zig b/src/canonicalize/test/roc_emitter_test.zig new file mode 100644 index 0000000000..9e87c5d9b9 --- /dev/null +++ b/src/canonicalize/test/roc_emitter_test.zig @@ -0,0 +1,230 @@ +//! Unit tests for the Roc emitter +//! +//! These tests verify that the emitter correctly converts CIR expressions +//! to valid Roc source code using manually constructed CIR nodes. + +const std = @import("std"); +const base = @import("base"); +const types = @import("types"); + +const Emitter = @import("../RocEmitter.zig"); +const ModuleEnv = @import("../ModuleEnv.zig"); +const CIR = @import("../CIR.zig"); + +const testing = std.testing; +const test_allocator = testing.allocator; + +fn createTestEnv(allocator: std.mem.Allocator, source: []const u8) !*ModuleEnv { + const module_env = try allocator.create(ModuleEnv); + module_env.* = try ModuleEnv.init(allocator, source); + return module_env; +} + +fn destroyTestEnv(allocator: std.mem.Allocator, module_env: *ModuleEnv) void { + module_env.deinit(); + allocator.destroy(module_env); +} + +// Basic expression tests + +test "emit integer literal" { + const module_env = try createTestEnv(test_allocator, "42"); + defer destroyTestEnv(test_allocator, module_env); + + var emitter = Emitter.init(test_allocator, module_env); + defer emitter.deinit(); + + const int_value = CIR.IntValue{ + .bytes = @bitCast(@as(i128, 42)), + .kind = .i128, + }; + const expr_idx = try module_env.store.addExpr(.{ + .e_num = .{ .value = int_value, .kind = .i64 }, + }, base.Region.zero()); + + try emitter.emitExpr(expr_idx); + try testing.expectEqualStrings("42", emitter.getOutput()); +} + +test "emit negative integer" { + const module_env = try createTestEnv(test_allocator, "-123"); + defer destroyTestEnv(test_allocator, module_env); + + var emitter = Emitter.init(test_allocator, module_env); + defer emitter.deinit(); + + const int_value = CIR.IntValue{ + .bytes = @bitCast(@as(i128, -123)), + .kind = .i128, + }; + const expr_idx = try module_env.store.addExpr(.{ + .e_num = .{ .value = int_value, .kind = .i64 }, + }, base.Region.zero()); + + try emitter.emitExpr(expr_idx); + try testing.expectEqualStrings("-123", emitter.getOutput()); +} + +test "emit empty record" { + const module_env = try createTestEnv(test_allocator, "{}"); + defer destroyTestEnv(test_allocator, module_env); + + var emitter = Emitter.init(test_allocator, module_env); + defer emitter.deinit(); + + const expr_idx = try module_env.store.addExpr(.{ + .e_empty_record = .{}, + }, base.Region.zero()); + + try emitter.emitExpr(expr_idx); + try testing.expectEqualStrings("{}", emitter.getOutput()); +} + +test "emit empty list" { + const module_env = try createTestEnv(test_allocator, "[]"); + defer destroyTestEnv(test_allocator, module_env); + + var emitter = Emitter.init(test_allocator, module_env); + defer emitter.deinit(); + + const expr_idx = try module_env.store.addExpr(.{ + .e_empty_list = .{}, + }, base.Region.zero()); + + try emitter.emitExpr(expr_idx); + try testing.expectEqualStrings("[]", emitter.getOutput()); +} + +test "emit identity lambda" { + const module_env = try createTestEnv(test_allocator, "|x| x"); + defer destroyTestEnv(test_allocator, module_env); + + var emitter = Emitter.init(test_allocator, module_env); + defer emitter.deinit(); + + // Create pattern for 'x' + const x_ident = try module_env.insertIdent(base.Ident.for_text("x")); + const x_pattern_idx = try module_env.store.addPattern(.{ + .assign = .{ .ident = x_ident }, + }, base.Region.zero()); + + // Create lookup expression for body + const body_idx = try module_env.store.addExpr(.{ + .e_lookup_local = .{ .pattern_idx = x_pattern_idx }, + }, base.Region.zero()); + + // Create lambda expression using scratch system + const start = module_env.store.scratchPatternTop(); + try module_env.store.addScratchPattern(x_pattern_idx); + const args_span = try module_env.store.patternSpanFrom(start); + + const lambda_idx = try module_env.store.addExpr(.{ + .e_lambda = .{ .args = args_span, .body = body_idx }, + }, base.Region.zero()); + + try emitter.emitExpr(lambda_idx); + try testing.expectEqualStrings("|x| x", emitter.getOutput()); +} + +test "emit tag with no arguments" { + const module_env = try createTestEnv(test_allocator, "True"); + defer destroyTestEnv(test_allocator, module_env); + + var emitter = Emitter.init(test_allocator, module_env); + defer emitter.deinit(); + + const true_ident = try module_env.insertIdent(base.Ident.for_text("True")); + const expr_idx = try module_env.store.addExpr(.{ + .e_zero_argument_tag = .{ + .closure_name = true_ident, + .variant_var = undefined, // not read by emitter + .ext_var = undefined, // not read by emitter + .name = true_ident, + }, + }, base.Region.zero()); + + try emitter.emitExpr(expr_idx); + try testing.expectEqualStrings("True", emitter.getOutput()); +} + +test "emit list with elements" { + const module_env = try createTestEnv(test_allocator, "[1, 2]"); + defer destroyTestEnv(test_allocator, module_env); + + var emitter = Emitter.init(test_allocator, module_env); + defer emitter.deinit(); + + // Create element expressions + const int_value_1 = CIR.IntValue{ + .bytes = @bitCast(@as(i128, 1)), + .kind = .i128, + }; + const elem1_idx = try module_env.store.addExpr(.{ + .e_num = .{ .value = int_value_1, .kind = .i64 }, + }, base.Region.zero()); + + const int_value_2 = CIR.IntValue{ + .bytes = @bitCast(@as(i128, 2)), + .kind = .i128, + }; + const elem2_idx = try module_env.store.addExpr(.{ + .e_num = .{ .value = int_value_2, .kind = .i64 }, + }, base.Region.zero()); + + // Create list using scratch system + const start = module_env.store.scratchExprTop(); + try module_env.store.addScratchExpr(elem1_idx); + try module_env.store.addScratchExpr(elem2_idx); + const elems_span = try module_env.store.exprSpanFrom(start); + + const list_idx = try module_env.store.addExpr(.{ + .e_list = .{ .elems = elems_span }, + }, base.Region.zero()); + + try emitter.emitExpr(list_idx); + try testing.expectEqualStrings("[1, 2]", emitter.getOutput()); +} + +test "emit function application" { + const module_env = try createTestEnv(test_allocator, "f(42)"); + defer destroyTestEnv(test_allocator, module_env); + + var emitter = Emitter.init(test_allocator, module_env); + defer emitter.deinit(); + + // Create pattern for 'f' (the function we're calling) + const f_ident = try module_env.insertIdent(base.Ident.for_text("f")); + const f_pattern_idx = try module_env.store.addPattern(.{ + .assign = .{ .ident = f_ident }, + }, base.Region.zero()); + + // Create function expression (lookup of f) + const func_idx = try module_env.store.addExpr(.{ + .e_lookup_local = .{ .pattern_idx = f_pattern_idx }, + }, base.Region.zero()); + + // Create argument expression + const int_value = CIR.IntValue{ + .bytes = @bitCast(@as(i128, 42)), + .kind = .i128, + }; + const arg_idx = try module_env.store.addExpr(.{ + .e_num = .{ .value = int_value, .kind = .i64 }, + }, base.Region.zero()); + + // Create call expression using scratch system + const start = module_env.store.scratchExprTop(); + try module_env.store.addScratchExpr(arg_idx); + const args_span = try module_env.store.exprSpanFrom(start); + + const call_idx = try module_env.store.addExpr(.{ + .e_call = .{ + .func = func_idx, + .args = args_span, + .called_via = .apply, + }, + }, base.Region.zero()); + + try emitter.emitExpr(call_idx); + try testing.expectEqualStrings("f(42)", emitter.getOutput()); +} diff --git a/src/canonicalize/test/scope_test.zig b/src/canonicalize/test/scope_test.zig index 39b0cfc3e1..8d4e98a832 100644 --- a/src/canonicalize/test/scope_test.zig +++ b/src/canonicalize/test/scope_test.zig @@ -23,7 +23,7 @@ const ScopeTestContext = struct { // heap allocate ModuleEnv for testing const module_env = try gpa.create(ModuleEnv); module_env.* = try ModuleEnv.init(gpa, ""); - try module_env.initCIRFields(gpa, "test"); + try module_env.initCIRFields("test"); return ScopeTestContext{ .self = try Can.init(module_env, undefined, null), diff --git a/src/canonicalize/test/type_decl_stmt_test.zig b/src/canonicalize/test/type_decl_stmt_test.zig new file mode 100644 index 0000000000..4c6cf2aa63 --- /dev/null +++ b/src/canonicalize/test/type_decl_stmt_test.zig @@ -0,0 +1,385 @@ +//! Tests for local type declarations in block contexts. +//! +//! Local type declarations allow defining type aliases, nominal types, and opaque types +//! within function bodies and blocks, scoped to that block. + +const std = @import("std"); +const parse = @import("parse"); +const base = @import("base"); +const types = @import("types"); +const ModuleEnv = @import("../ModuleEnv.zig"); +const Can = @import("../Can.zig"); +const CIR = @import("../CIR.zig"); +const TestEnv = @import("TestEnv.zig").TestEnv; + +const testing = std.testing; +const Ident = base.Ident; +const Statement = CIR.Statement; + +test "local type alias is parsed and canonicalized" { + const source = + \\|_| { + \\ MyNum : U64 + \\ 42 + \\} + ; + + var test_env = try TestEnv.init(source); + defer test_env.deinit(); + + const result = try test_env.canonicalizeExpr(); + try testing.expect(result != null); + + // Check diagnostics - should have no errors + const diagnostics = try test_env.getDiagnostics(); + defer testing.allocator.free(diagnostics); + + var error_count: usize = 0; + for (diagnostics) |diag| { + switch (diag) { + .not_implemented => error_count += 1, + .undeclared_type => error_count += 1, + .ident_not_in_scope => error_count += 1, + else => {}, + } + } + try testing.expectEqual(@as(usize, 0), error_count); +} + +test "local nominal type is parsed and canonicalized" { + const source = + \\|_| { + \\ Counter := U64 + \\ 42 + \\} + ; + + var test_env = try TestEnv.init(source); + defer test_env.deinit(); + + const result = try test_env.canonicalizeExpr(); + try testing.expect(result != null); + + const diagnostics = try test_env.getDiagnostics(); + defer testing.allocator.free(diagnostics); + + var error_count: usize = 0; + for (diagnostics) |diag| { + switch (diag) { + .not_implemented => error_count += 1, + .undeclared_type => error_count += 1, + .ident_not_in_scope => error_count += 1, + else => {}, + } + } + try testing.expectEqual(@as(usize, 0), error_count); +} + +test "local opaque type is parsed and canonicalized" { + // Use U8 instead of Str since Str is an auto-imported type, not a builtin + const source = + \\|_| { + \\ Secret :: U8 + \\ 42 + \\} + ; + + var test_env = try TestEnv.init(source); + defer test_env.deinit(); + + const result = try test_env.canonicalizeExpr(); + try testing.expect(result != null); + + const diagnostics = try test_env.getDiagnostics(); + defer testing.allocator.free(diagnostics); + + var error_count: usize = 0; + for (diagnostics) |diag| { + switch (diag) { + .not_implemented => error_count += 1, + .undeclared_type => error_count += 1, + .ident_not_in_scope => error_count += 1, + else => {}, + } + } + try testing.expectEqual(@as(usize, 0), error_count); +} + +test "nested blocks with local types" { + // Use builtin types (U64, U8) instead of Str + const source = + \\|_| { + \\ OuterType : U64 + \\ inner = { + \\ InnerType : U8 + \\ 42 + \\ } + \\ inner + \\} + ; + + var test_env = try TestEnv.init(source); + defer test_env.deinit(); + + const result = try test_env.canonicalizeExpr(); + try testing.expect(result != null); + + const diagnostics = try test_env.getDiagnostics(); + defer testing.allocator.free(diagnostics); + + var error_count: usize = 0; + for (diagnostics) |diag| { + switch (diag) { + .not_implemented => error_count += 1, + .undeclared_type => error_count += 1, + .ident_not_in_scope => error_count += 1, + else => {}, + } + } + try testing.expectEqual(@as(usize, 0), error_count); +} + +test "multiple local types in same block" { + // Use builtin types (U64, U8) instead of Str + const source = + \\|_| { + \\ First : U64 + \\ Second : U8 + \\ Third : { a: U64, b: U8 } + \\ 42 + \\} + ; + + var test_env = try TestEnv.init(source); + defer test_env.deinit(); + + const result = try test_env.canonicalizeExpr(); + try testing.expect(result != null); + + const diagnostics = try test_env.getDiagnostics(); + defer testing.allocator.free(diagnostics); + + var error_count: usize = 0; + for (diagnostics) |diag| { + switch (diag) { + .not_implemented => error_count += 1, + .undeclared_type => error_count += 1, + .ident_not_in_scope => error_count += 1, + else => {}, + } + } + try testing.expectEqual(@as(usize, 0), error_count); +} + +test "local type with type parameters" { + // Type parameters use parentheses syntax: MyList(a) + const source = + \\|_| { + \\ MyWrapper(a) : List(a) + \\ 42 + \\} + ; + + var test_env = try TestEnv.init(source); + defer test_env.deinit(); + + const result = try test_env.canonicalizeExpr(); + try testing.expect(result != null); + + const diagnostics = try test_env.getDiagnostics(); + defer testing.allocator.free(diagnostics); + + var error_count: usize = 0; + for (diagnostics) |diag| { + switch (diag) { + .not_implemented => error_count += 1, + .undeclared_type => error_count += 1, + .ident_not_in_scope => error_count += 1, + else => {}, + } + } + try testing.expectEqual(@as(usize, 0), error_count); +} + +test "expression that looks like type decl but isn't - record field" { + // Record fields use lowercase names with colons: { name: "value" } + // This should NOT be parsed as a type declaration + const source = + \\|_| { + \\ x = { name: 42, count: 10 } + \\ x + \\} + ; + + var test_env = try TestEnv.init(source); + defer test_env.deinit(); + + const result = try test_env.canonicalizeExpr(); + try testing.expect(result != null); + + // This should parse as a record, not as a type declaration + // No type-related errors expected + const diagnostics = try test_env.getDiagnostics(); + defer testing.allocator.free(diagnostics); + + var type_decl_errors: usize = 0; + for (diagnostics) |diag| { + switch (diag) { + .undeclared_type => type_decl_errors += 1, + else => {}, + } + } + try testing.expectEqual(@as(usize, 0), type_decl_errors); +} + +test "local type alias can be used in annotation" { + // Test that a locally defined type alias can be used in a type annotation + const source = + \\|_| { + \\ MyNum : U64 + \\ x : MyNum + \\ x = 42 + \\ x + \\} + ; + + var test_env = try TestEnv.init(source); + defer test_env.deinit(); + + const result = try test_env.canonicalizeExpr(); + try testing.expect(result != null); + + const diagnostics = try test_env.getDiagnostics(); + defer testing.allocator.free(diagnostics); + + var error_count: usize = 0; + for (diagnostics) |diag| { + switch (diag) { + .not_implemented => error_count += 1, + .undeclared_type => error_count += 1, + .ident_not_in_scope => error_count += 1, + else => {}, + } + } + try testing.expectEqual(@as(usize, 0), error_count); +} + +test "scopeLookupTypeDecl API is accessible" { + const gpa = testing.allocator; + const source = ""; + + var env = try ModuleEnv.init(gpa, source); + defer env.deinit(); + + try env.initCIRFields("test"); + + var ast = try parse.parseExpr(&env.common, gpa); + defer ast.deinit(gpa); + + var can = try Can.init(&env, &ast, null); + defer can.deinit(); + + // Enter a scope + try can.scopeEnter(gpa, true); + + // Look up a type that doesn't exist - should return null + const my_type_ident = try env.insertIdent(Ident.for_text("MyType")); + const type_lookup = can.scopeLookupTypeDecl(my_type_ident); + + try testing.expect(type_lookup == null); +} + +test "introduceType API is accessible" { + const gpa = testing.allocator; + const source = ""; + + var env = try ModuleEnv.init(gpa, source); + defer env.deinit(); + + try env.initCIRFields("test"); + + var ast = try parse.parseExpr(&env.common, gpa); + defer ast.deinit(gpa); + + var can = try Can.init(&env, &ast, null); + defer can.deinit(); + + // Enter a scope for local type declarations + try can.scopeEnter(gpa, true); + + // Create a type header manually + const type_name = try env.insertIdent(Ident.for_text("LocalType")); + const type_header = CIR.TypeHeader{ + .name = type_name, + .relative_name = type_name, + .args = CIR.TypeAnno.Span{ .span = base.DataSpan.empty() }, + }; + const header_idx = try env.addTypeHeader(type_header, base.Region.zero()); + + // Create a type alias statement + const alias_stmt = Statement{ + .s_alias_decl = .{ + .header = header_idx, + .anno = .placeholder, + }, + }; + const stmt_idx = try env.addStatement(alias_stmt, base.Region.zero()); + + // Introduce the type into scope + try can.introduceType(type_name, stmt_idx, base.Region.zero()); + + // Verify the type is now in scope + const type_lookup = can.scopeLookupTypeDecl(type_name); + try testing.expect(type_lookup != null); + try testing.expect(type_lookup.? == stmt_idx); +} + +test "local type scoping - not visible after exiting block" { + const gpa = testing.allocator; + const source = ""; + + var env = try ModuleEnv.init(gpa, source); + defer env.deinit(); + + try env.initCIRFields("test"); + + var ast = try parse.parseExpr(&env.common, gpa); + defer ast.deinit(gpa); + + var can = try Can.init(&env, &ast, null); + defer can.deinit(); + + // Enter outer scope + try can.scopeEnter(gpa, true); + + // Enter inner scope + try can.scopeEnter(gpa, false); + + // Create and introduce a local type in the inner scope + const type_name = try env.insertIdent(Ident.for_text("InnerType")); + const type_header = CIR.TypeHeader{ + .name = type_name, + .relative_name = type_name, + .args = CIR.TypeAnno.Span{ .span = base.DataSpan.empty() }, + }; + const header_idx = try env.addTypeHeader(type_header, base.Region.zero()); + const alias_stmt = Statement{ + .s_alias_decl = .{ + .header = header_idx, + .anno = .placeholder, + }, + }; + const stmt_idx = try env.addStatement(alias_stmt, base.Region.zero()); + try can.introduceType(type_name, stmt_idx, base.Region.zero()); + + // Type should be visible in inner scope + const lookup_in_inner = can.scopeLookupTypeDecl(type_name); + try testing.expect(lookup_in_inner != null); + + // Exit inner scope + try can.scopeExit(gpa); + + // Type should NOT be visible in outer scope anymore + const lookup_in_outer = can.scopeLookupTypeDecl(type_name); + try testing.expect(lookup_in_outer == null); +} diff --git a/src/check/Check.zig b/src/check/Check.zig index 52ea9c0a98..674a074ae7 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -9,28 +9,278 @@ const tracy = @import("tracy"); const collections = @import("collections"); const types_mod = @import("types"); const can = @import("can"); +const builtins = @import("builtins"); const copy_import = @import("copy_import.zig"); const unifier = @import("unify.zig"); const occurs = @import("occurs.zig"); const problem = @import("problem.zig"); +const snapshot_mod = @import("snapshot.zig"); +const ExposedItems = collections.ExposedItems; const CIR = can.CIR; const CommonEnv = base.CommonEnv; const ModuleEnv = can.ModuleEnv; const Allocator = std.mem.Allocator; const Ident = base.Ident; const Region = base.Region; -const Instantiate = types_mod.instantiate.Instantiate; +const DeferredConstraintCheck = unifier.DeferredConstraintCheck; +const StaticDispatchConstraint = types_mod.StaticDispatchConstraint; const Func = types_mod.Func; const Var = types_mod.Var; +const Flex = types_mod.Flex; +const Rigid = types_mod.Rigid; const Content = types_mod.Content; +const Rank = types_mod.Rank; +const Mark = types_mod.Mark; +const Num = types_mod.Num; const testing = std.testing; +const Instantiator = types_mod.instantiate.Instantiator; +const Generalizer = types_mod.generalize.Generalizer; +const VarPool = types_mod.generalize.VarPool; const SnapshotStore = @import("snapshot.zig").Store; const ProblemStore = @import("problem.zig").Store; +const is_freestanding = builtin.os.tag == .freestanding; + +/// Deferred numeric literal for compile-time validation +/// These are collected during type checking and validated during comptime evaluation +pub const DeferredNumericLiteral = struct { + /// The e_num expression index + expr_idx: CIR.Expr.Idx, + /// The type variable that the literal unified with + type_var: Var, + /// The from_numeral constraint attached to this literal + constraint: StaticDispatchConstraint, + /// Source region for error reporting + region: Region, + + pub const SafeList = collections.SafeList(@This()); +}; + const Self = @This(); +gpa: std.mem.Allocator, +// This module's types store +types: *types_mod.Store, +/// This module's env +cir: *ModuleEnv, +/// A list of regions. Parallel with type vars & CIR nodes +regions: *Region.List, +/// List of directly imported module. Import indexes in CIR refer to this list +imported_modules: []const *const ModuleEnv, +/// Map of auto-imported type names (like "Str", "List", "Bool") to their defining modules. +/// This is used to resolve type names that are automatically available without explicit imports. +auto_imported_types: ?*const std.AutoHashMap(Ident.Idx, can.Can.AutoImportedType), +/// Builtin type context for the module being type-checked +builtin_ctx: BuiltinContext, +/// type snapshots used in error messages +snapshots: SnapshotStore, +/// type problems +problems: ProblemStore, +/// import mapping for auto-imported builtin types (for error display) +import_mapping: @import("types").import_mapping.ImportMapping, +/// reusable scratch arrays used in unification +unify_scratch: unifier.Scratch, +/// reusable scratch arrays used in occurs check +occurs_scratch: occurs.Scratch, +/// free vars collected when generation types from annotation +anno_free_vars: base.Scratch(FreeVar), +/// free vars collected when generation types from type decls +decl_free_vars: base.Scratch(FreeVar), +/// annos we've already seen when generation a type from an annotation +seen_annos: std.AutoHashMap(CIR.TypeAnno.Idx, Var), +/// A pool of solver envs +env_pool: EnvPool, +/// wrapper around generalization, contains some internal state used to do it's work +generalizer: Generalizer, +/// A map from one var to another. Used in instantiation and var copying +var_map: std.AutoHashMap(Var, Var), +/// A map from one var to another. Used to apply type arguments in instantiation +rigid_var_substitutions: std.AutoHashMapUnmanaged(Ident.Idx, Var), +/// scratch vars used to build up intermediate lists, used for various things +scratch_vars: base.Scratch(Var), +/// scratch tags used to build up intermediate lists, used for various things +scratch_tags: base.Scratch(types_mod.Tag), +/// scratch record fields used to build up intermediate lists, used for various things +scratch_record_fields: base.Scratch(types_mod.RecordField), +/// scratch static dispatch constraints used to build up intermediate lists, used for various things +scratch_static_dispatch_constraints: base.Scratch(ScratchStaticDispatchConstraint), +/// Stack of type variables currently being constraint-checked, used to detect recursive constraints +/// When a var appears in this stack while we're checking its constraints, we've detected recursion +constraint_check_stack: std.ArrayList(Var), +// Cache for imported types. This cache lives for the entire type-checking session +/// of a module, so the same imported type can be reused across the entire module. +import_cache: ImportCache, +/// Maps variables to the expressions that constrained them (for better error regions) +constraint_origins: std.AutoHashMap(Var, Var), +/// Copied Bool type from Bool module (for use in if conditions, etc.) +bool_var: Var, +/// Copied Str type from Builtin module (for use in string literals, etc.) +str_var: Var, +/// Map representation of Ident -> Var, used in checking static dispatch constraints +ident_to_var_map: std.AutoHashMap(Ident.Idx, Var), +/// Map representation all top level patterns, and if we've processed them yet +top_level_ptrns: std.AutoHashMap(CIR.Pattern.Idx, DefProcessed), +/// The expected return type of the enclosing function, if any. +/// Used to correctly type-check `return` expressions inside loops etc. +enclosing_func_return_type: ?Var, +/// The name of the enclosing function, if known. +/// Used to provide better error messages when type checking lambda arguments. +enclosing_func_name: ?Ident.Idx, +/// Type writer for formatting types at snapshot time +type_writer: types_mod.TypeWriter, + +/// A map of rigid variables that we build up during a branch of type checking +const FreeVar = struct { ident: base.Ident.Idx, var_: Var }; + +/// A def + processing data +const DefProcessed = struct { def_idx: CIR.Def.Idx, status: HasProcessed }; + +/// Indicates if something has been processed or not +const HasProcessed = enum { processed, processing, not_processed }; + +/// A struct scratch info about a static dispatch constraint +const ScratchStaticDispatchConstraint = struct { + var_: Var, + constraint: types_mod.StaticDispatchConstraint, +}; + +/// Context for type checking: module identity, builtin type references, and the Builtin module itself. +/// This is passed to Check.init() to provide access to auto-imported types from Builtin. +pub const BuiltinContext = struct { + /// The name of the module being type-checked + module_name: base.Ident.Idx, + /// Statement index of Bool type in the current module (injected from Builtin.bin) + bool_stmt: can.CIR.Statement.Idx, + /// Statement index of Try type in the current module (injected from Builtin.bin) + try_stmt: can.CIR.Statement.Idx, + /// Statement index of Str type in the current module (injected from Builtin.bin) + str_stmt: can.CIR.Statement.Idx, + /// Direct reference to the Builtin module env (null when compiling Builtin module itself) + builtin_module: ?*const ModuleEnv, + /// Indices of auto-imported types in the Builtin module (null when compiling Builtin module itself) + builtin_indices: ?can.CIR.BuiltinIndices, +}; + +/// Init type solver +/// Does *not* own types_store or cir, but *does* own other fields +pub fn init( + gpa: std.mem.Allocator, + types: *types_mod.Store, + cir: *const ModuleEnv, + imported_modules: []const *const ModuleEnv, + auto_imported_types: ?*const std.AutoHashMap(Ident.Idx, can.Can.AutoImportedType), + regions: *Region.List, + builtin_ctx: BuiltinContext, +) std.mem.Allocator.Error!Self { + const mutable_cir = @constCast(cir); + var import_mapping = try createImportMapping( + gpa, + mutable_cir.getIdentStore(), + cir, + builtin_ctx.builtin_module, + builtin_ctx.builtin_indices, + auto_imported_types, + ); + errdefer import_mapping.deinit(); + + return .{ + .gpa = gpa, + .types = types, + .cir = mutable_cir, + .imported_modules = imported_modules, + .auto_imported_types = auto_imported_types, + .regions = regions, + .builtin_ctx = builtin_ctx, + .snapshots = try SnapshotStore.initCapacity(gpa, 512), + .problems = try ProblemStore.initCapacity(gpa, 64), + .import_mapping = import_mapping, + .unify_scratch = try unifier.Scratch.init(gpa), + .occurs_scratch = try occurs.Scratch.init(gpa), + .anno_free_vars = try base.Scratch(FreeVar).init(gpa), + .decl_free_vars = try base.Scratch(FreeVar).init(gpa), + .seen_annos = std.AutoHashMap(CIR.TypeAnno.Idx, Var).init(gpa), + .env_pool = try EnvPool.init(gpa), + .generalizer = try Generalizer.init(gpa, types), + .var_map = std.AutoHashMap(Var, Var).init(gpa), + .rigid_var_substitutions = std.AutoHashMapUnmanaged(Ident.Idx, Var){}, + .scratch_vars = try base.Scratch(types_mod.Var).init(gpa), + .scratch_tags = try base.Scratch(types_mod.Tag).init(gpa), + .scratch_record_fields = try base.Scratch(types_mod.RecordField).init(gpa), + .scratch_static_dispatch_constraints = try base.Scratch(ScratchStaticDispatchConstraint).init(gpa), + .constraint_check_stack = try std.ArrayList(Var).initCapacity(gpa, 0), + .import_cache = ImportCache{}, + .constraint_origins = std.AutoHashMap(Var, Var).init(gpa), + .bool_var = undefined, // Will be initialized in copyBuiltinTypes() + .str_var = undefined, // Will be initialized in copyBuiltinTypes() + .ident_to_var_map = std.AutoHashMap(Ident.Idx, Var).init(gpa), + .top_level_ptrns = std.AutoHashMap(CIR.Pattern.Idx, DefProcessed).init(gpa), + .enclosing_func_return_type = null, + .enclosing_func_name = null, + // Initialize with null import_mapping - caller should call fixupTypeWriter() after storing Check + .type_writer = try types_mod.TypeWriter.initFromParts(gpa, types, mutable_cir.getIdentStore(), null), + }; +} + +/// Call this after Check has been stored at its final location to set up the import_mapping pointer. +/// This is needed because returning Check by value invalidates the pointer set during init. +pub fn fixupTypeWriter(self: *Self) void { + self.type_writer.setImportMapping(&self.import_mapping); +} + +/// Deinit owned fields +pub fn deinit(self: *Self) void { + self.problems.deinit(self.gpa); + self.snapshots.deinit(); + self.import_mapping.deinit(); + self.unify_scratch.deinit(); + self.occurs_scratch.deinit(); + self.anno_free_vars.deinit(); + self.decl_free_vars.deinit(); + self.seen_annos.deinit(); + self.env_pool.deinit(); + self.generalizer.deinit(self.gpa); + self.var_map.deinit(); + self.rigid_var_substitutions.deinit(self.gpa); + self.scratch_vars.deinit(); + self.scratch_tags.deinit(); + self.scratch_record_fields.deinit(); + self.scratch_static_dispatch_constraints.deinit(); + self.constraint_check_stack.deinit(self.gpa); + self.import_cache.deinit(self.gpa); + self.constraint_origins.deinit(); + self.ident_to_var_map.deinit(); + self.top_level_ptrns.deinit(); + self.type_writer.deinit(); +} + +/// Assert that type vars and regions in sync +pub inline fn debugAssertArraysInSync(self: *const Self) void { + if (builtin.mode == .Debug) { + const region_nodes = self.regions.len(); + const type_nodes = self.types.len(); + if (!(region_nodes == type_nodes)) { + std.debug.panic( + "Arrays out of sync:\n type_nodes={}\n region_nodes={}\n ", + .{ type_nodes, region_nodes }, + ); + } + } +} + +/// Fills the type store with fresh variables up to the number of regions +inline fn ensureTypeStoreIsFilled(self: *Self) Allocator.Error!void { + const region_nodes: usize = @intCast(self.regions.len()); + const type_nodes: usize = @intCast(self.types.len()); + try self.types.ensureTotalCapacity(region_nodes); + for (type_nodes..region_nodes) |_| { + _ = self.types.appendFromContentAssumeCapacity(.{ .flex = Flex.init() }, @enumFromInt(15)); + } +} + +// import caches // + /// Key for the import cache: module index + expression index in that module const ImportCacheKey = struct { module_idx: CIR.Import.Idx, @@ -66,115 +316,124 @@ const ImportCache = std.HashMapUnmanaged(ImportCacheKey, Var, struct { } }, 80); -gpa: std.mem.Allocator, -// not owned -types: *types_mod.Store, -cir: *ModuleEnv, -regions: *Region.List, -other_modules: []const *ModuleEnv, -// owned -snapshots: SnapshotStore, -problems: ProblemStore, -unify_scratch: unifier.Scratch, -occurs_scratch: occurs.Scratch, -var_map: std.AutoHashMap(Var, Var), -annotation_rigid_var_subs: Instantiate.RigidToFlexSubs, -anonymous_rigid_var_subs: Instantiate.RigidToFlexSubs, -/// Cache for imported types. This cache lives for the entire type-checking session -/// of a module, so the same imported type can be reused across the entire module. -import_cache: ImportCache, -/// Maps variables to the expressions that constrained them (for better error regions) -constraint_origins: std.AutoHashMap(Var, Var), +// env // -/// Init type solver -/// Does *not* own types_store or cir, but *does* own other fields -pub fn init( - gpa: std.mem.Allocator, - types: *types_mod.Store, - cir: *const ModuleEnv, - other_modules: []const *ModuleEnv, - regions: *Region.List, -) std.mem.Allocator.Error!Self { - return .{ - .gpa = gpa, - .types = types, - .cir = @constCast(cir), - .other_modules = other_modules, - .regions = regions, - .snapshots = try SnapshotStore.initCapacity(gpa, 512), - .problems = try ProblemStore.initCapacity(gpa, 64), - .unify_scratch = try unifier.Scratch.init(gpa), - .occurs_scratch = try occurs.Scratch.init(gpa), - .var_map = std.AutoHashMap(Var, Var).init(gpa), - .annotation_rigid_var_subs = try Instantiate.RigidToFlexSubs.init(gpa), - .anonymous_rigid_var_subs = try Instantiate.RigidToFlexSubs.init(gpa), - .import_cache = ImportCache{}, - .constraint_origins = std.AutoHashMap(Var, Var).init(gpa), - }; -} +/// Solver env +const Env = struct { + /// Pool of variables created during solving, use by let-polymorphism + var_pool: VarPool, + /// Deferred static dispatch constraints - accumulated during type checking, + /// then solved for at the end + deferred_static_dispatch_constraints: DeferredConstraintCheck.SafeList, -/// Deinit owned fields -pub fn deinit(self: *Self) void { - self.problems.deinit(self.gpa); - self.snapshots.deinit(); - self.unify_scratch.deinit(); - self.occurs_scratch.deinit(); - self.var_map.deinit(); - self.annotation_rigid_var_subs.deinit(self.gpa); - self.anonymous_rigid_var_subs.deinit(self.gpa); - self.import_cache.deinit(self.gpa); - self.constraint_origins.deinit(); -} + fn init( + gpa: std.mem.Allocator, + at: Rank, + ) std.mem.Allocator.Error!Env { + var pool = try VarPool.init(gpa); + pool.current_rank = at; -/// Assert that type vars and regions in sync -pub inline fn debugAssertArraysInSync(self: *const Self) void { - if (builtin.mode == .Debug) { - const region_nodes = self.regions.len(); - const type_nodes = self.types.len(); - if (!(region_nodes == type_nodes)) { - std.debug.panic( - "Arrays out of sync:\n type_nodes={}\n region_nodes={}\n ", - .{ type_nodes, region_nodes }, - ); - } + return .{ + .var_pool = pool, + .deferred_static_dispatch_constraints = try DeferredConstraintCheck.SafeList.initCapacity(gpa, 32), + }; } -} + + fn deinit(self: *Env, gpa: std.mem.Allocator) void { + self.var_pool.deinit(); + self.deferred_static_dispatch_constraints.deinit(gpa); + } + + /// Resets internal state of env and set rank to generalized + fn reset(self: *Env) void { + self.var_pool.current_rank = .generalized; + self.var_pool.clearRetainingCapacity(); + self.deferred_static_dispatch_constraints.items.clearRetainingCapacity(); + } + + fn rank(self: *const Env) Rank { + return self.var_pool.current_rank; + } +}; // unify // -/// Unify two types -pub fn unify(self: *Self, a: Var, b: Var) std.mem.Allocator.Error!unifier.Result { +/// Unify two types where `a` is the expected type and `b` is the actual type +fn unify(self: *Self, a: Var, b: Var, env: *Env) std.mem.Allocator.Error!unifier.Result { + return self.unifyWithCtx(a, b, env, .anon); +} + +/// Unify two types where `a` is the expected type and `b` is the actual type +/// In error messages, this function will indicate that `a` as "from an annotation" +fn unifyFromAnno(self: *Self, a: Var, b: Var, env: *Env) std.mem.Allocator.Error!unifier.Result { + return self.unifyWithCtx(a, b, env, .anno); +} + +/// Unify two types where `a` is the expected type and `b` is the actual type +/// Accepts a config that indicates if `a` is from an annotation or not +fn unifyWithCtx(self: *Self, a: Var, b: Var, env: *Env, ctx: unifier.Conf.Ctx) std.mem.Allocator.Error!unifier.Result { const trace = tracy.trace(@src()); defer trace.end(); // Before unification, check if either variable has constraint origins - const a_origin = self.constraint_origins.get(a); - const b_origin = self.constraint_origins.get(b); - const constraint_origin_var = a_origin orelse b_origin; + // We need to look up constraint origins by walking through the type structure + const constraint_origin_var = self.findConstraintOriginForVars(a, b); - const result = try unifier.unifyWithConstraintOrigin( + // Unify + const result = try unifier.unifyWithConf( self.cir, self.types, &self.problems, &self.snapshots, + &self.type_writer, &self.unify_scratch, &self.occurs_scratch, a, b, - false, // from_annotation = false - constraint_origin_var, + unifier.Conf{ + .ctx = ctx, + .constraint_origin_var = constraint_origin_var, + }, ); // After successful unification, propagate constraint origins to both variables if (result == .ok) { - if (a_origin) |origin| { + if (constraint_origin_var) |origin| { + try self.constraint_origins.put(a, origin); try self.constraint_origins.put(b, origin); } - if (b_origin) |origin| { - try self.constraint_origins.put(a, origin); - } } + // Set regions and add to the current rank all variables created during unification. + // + // We assign all fresh variables the region of `b` (the "actual" type), since `a` is + // typically the "expected" type from an annotation. This heuristic works well for + // most cases but can be imprecise for deeply nested unifications where fresh variables + // are created for sub-components (e.g., record fields, tag payloads). In those cases, + // error messages may point to the outer expression rather than the specific field. + // + // A more precise solution would track the origin of each fresh variable during + // unification and propagate that back, but the current approach is sufficient for + // typical error reporting scenarios. + const region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(b)); + for (self.unify_scratch.fresh_vars.items.items) |fresh_var| { + // Set the rank + const fresh_rank = self.types.resolveVar(fresh_var).desc.rank; + try env.var_pool.addVarToRank(fresh_var, fresh_rank); + + // Set the region + try self.fillInRegionsThrough(fresh_var); + self.setRegionAt(fresh_var, region); + } + + // Copy any constraints created during unification into our own array + for (self.unify_scratch.deferred_constraints.items.items) |deferred_constraint| { + _ = try env.deferred_static_dispatch_constraints.append(self.gpa, deferred_constraint); + } + + // Ensure arrays are in sync + self.debugAssertArraysInSync(); + return result; } @@ -193,74 +452,20 @@ fn findConstraintOriginForVars(self: *Self, a: Var, b: Var) ?Var { // Fallback: if we have any constraint origins recorded (indicating dot access expressions), // and we haven't found a direct match, look for constraint origins that might be related - if (self.constraint_origins.count() > 0) { - var it = self.constraint_origins.iterator(); - while (it.next()) |entry| { - const origin = entry.value_ptr.*; - // Return the first constraint origin we find - this is specifically for the Color.md case - // where constraint origins exist but don't directly match the unification variables - return origin; - } - } + // if (self.constraint_origins.count() > 0) { + // var it = self.constraint_origins.iterator(); + // while (it.next()) |entry| { + // const origin = entry.value_ptr.*; + // // Return the first constraint origin we find - this is specifically for the Color.md case + // // where constraint origins exist but don't directly match the unification variables + // return origin; + // } + // } return null; } -/// Unify two variables where the second represents an annotation type. -/// This sets from_annotation=true to ensure proper error region highlighting. -pub fn unifyWithAnnotation(self: *Self, a: Var, b: Var) std.mem.Allocator.Error!unifier.Result { - const trace = tracy.trace(@src()); - defer trace.end(); - - // Before unification, check if either variable has constraint origins - // We need to look up constraint origins by walking through the type structure - const constraint_origin_var = self.findConstraintOriginForVars(a, b); - - const result = try unifier.unifyWithConstraintOrigin( - self.cir, - self.types, - &self.problems, - &self.snapshots, - &self.unify_scratch, - &self.occurs_scratch, - a, - b, - true, // from_annotation = true - constraint_origin_var, - ); - - // After successful unification, propagate constraint origins to both variables - if (result == .ok) { - if (constraint_origin_var) |origin| { - try self.constraint_origins.put(a, origin); - try self.constraint_origins.put(b, origin); - } - } - - return result; -} - -/// Unify two variables with a specific constraint origin for better error reporting. -/// The constraint_origin_var should point to the expression that created the constraint. -pub fn unifyWithConstraintOrigin(self: *Self, a: Var, b: Var, constraint_origin_var: Var) std.mem.Allocator.Error!unifier.Result { - const trace = tracy.trace(@src()); - defer trace.end(); - - return try unifier.unifyWithConstraintOrigin( - self.cir, - self.types, - &self.problems, - &self.snapshots, - &self.unify_scratch, - &self.occurs_scratch, - a, - b, - false, // from_annotation = false - constraint_origin_var, - ); -} - -// instantiate // +// instantiate // const InstantiateRegionBehavior = union(enum) { explicit: Region, @@ -268,36 +473,97 @@ const InstantiateRegionBehavior = union(enum) { use_last_var, }; -const RigidVarBehavior = union(enum) { - use_cached_rigid_vars, - rollback_rigid_vars, -}; - -/// Instantiate a variable +/// Instantiate a variable, substituting any encountered rigids with flex vars +/// +/// Note that the the rigid var structure will be preserved. +/// E.g. `a -> a`, `a` will reference the same new flex var fn instantiateVar( self: *Self, var_to_instantiate: Var, - rigid_to_flex_subs: *Instantiate.RigidToFlexSubs, + env: *Env, region_behavior: InstantiateRegionBehavior, ) std.mem.Allocator.Error!Var { - self.var_map.clearRetainingCapacity(); + var instantiate_ctx = Instantiator{ + .store = self.types, + .idents = self.cir.getIdentStoreConst(), + .var_map = &self.var_map, - var instantiate = Instantiate.init(self.types, self.cir.getIdentStore(), &self.var_map); - var instantiate_ctx = Instantiate.Ctx{ - .rigid_var_subs = rigid_to_flex_subs, + .current_rank = env.rank(), + .rigid_behavior = .fresh_flex, }; - const instantiated_var = try instantiate.instantiateVar(var_to_instantiate, &instantiate_ctx); + return self.instantiateVarHelp(var_to_instantiate, &instantiate_ctx, env, region_behavior); +} + +/// Instantiate a variable, substituting any encountered rigids with *new* rigid vars +/// +/// Note that the the rigid var structure will be preserved. +/// E.g. `a -> a`, `a` will reference the same new rigid var +fn instantiateVarPreserveRigids( + self: *Self, + var_to_instantiate: Var, + env: *Env, + region_behavior: InstantiateRegionBehavior, +) std.mem.Allocator.Error!Var { + var instantiate_ctx = Instantiator{ + .store = self.types, + .idents = self.cir.getIdentStoreConst(), + .var_map = &self.var_map, + + .current_rank = env.rank(), + .rigid_behavior = .fresh_rigid, + }; + return self.instantiateVarHelp(var_to_instantiate, &instantiate_ctx, env, region_behavior); +} + +/// Instantiate a variable +fn instantiateVarWithSubs( + self: *Self, + var_to_instantiate: Var, + subs: *std.AutoHashMapUnmanaged(Ident.Idx, Var), + env: *Env, + region_behavior: InstantiateRegionBehavior, +) std.mem.Allocator.Error!Var { + var instantiate_ctx = Instantiator{ + .store = self.types, + .idents = self.cir.getIdentStoreConst(), + .var_map = &self.var_map, + + .current_rank = env.rank(), + .rigid_behavior = .{ .substitute_rigids = subs }, + }; + return self.instantiateVarHelp(var_to_instantiate, &instantiate_ctx, env, region_behavior); +} + +/// Instantiate a variable +fn instantiateVarHelp( + self: *Self, + var_to_instantiate: Var, + instantiator: *Instantiator, + env: *Env, + region_behavior: InstantiateRegionBehavior, +) std.mem.Allocator.Error!Var { + // First, reset state + instantiator.var_map.clearRetainingCapacity(); + + // Then, instantiate the variable with the provided context + const instantiated_var = try instantiator.instantiateVar(var_to_instantiate); // If we had to insert any new type variables, ensure that we have // corresponding regions for them. This is essential for error reporting. const root_instantiated_region = self.regions.get(@enumFromInt(@intFromEnum(var_to_instantiate))).*; - if (self.var_map.count() > 0) { - var iterator = self.var_map.iterator(); + if (instantiator.var_map.count() > 0) { + var iterator = instantiator.var_map.iterator(); while (iterator.next()) |x| { // Get the newly created var const fresh_var = x.value_ptr.*; - try self.fillInRegionsThrough(fresh_var); + const fresh_resolved = self.types.resolveVar(fresh_var); + + // Add to pool + try env.var_pool.addVarToRank(fresh_var, fresh_resolved.desc.rank); + + // Set the region + try self.fillInRegionsThrough(fresh_var); switch (region_behavior) { .explicit => |region| { self.setRegionAt(fresh_var, region); @@ -314,61 +580,16 @@ fn instantiateVar( } } + // Add the var to the right rank + try env.var_pool.addVarToRank(instantiated_var, instantiator.current_rank); + // Assert that we have regions for every type variable self.debugAssertArraysInSync(); + // Return the instantiated var return instantiated_var; } -/// Instantiate a variable -fn instantiateVarAnon( - self: *Self, - var_to_instantiate: Var, - region_behavior: InstantiateRegionBehavior, -) std.mem.Allocator.Error!Var { - self.anonymous_rigid_var_subs.items.clearRetainingCapacity(); - const var_ = self.instantiateVar(var_to_instantiate, &self.anonymous_rigid_var_subs, region_behavior); - return var_; -} - -// copy type from other module // - -/// Instantiate a variable, writing su -fn copyVar( - self: *Self, - other_module_var: Var, - other_module_env: *ModuleEnv, -) std.mem.Allocator.Error!Var { - self.var_map.clearRetainingCapacity(); - const copied_var = try copy_import.copyVar( - &other_module_env.*.types, - self.types, - other_module_var, - &self.var_map, - other_module_env.getIdentStore(), - self.cir.getIdentStore(), - self.gpa, - ); - - // If we had to insert any new type variables, ensure that we have - // corresponding regions for them. This is essential for error reporting. - if (self.var_map.count() > 0) { - var iterator = self.var_map.iterator(); - while (iterator.next()) |x| { - // Get the newly created var - const fresh_var = x.value_ptr.*; - try self.fillInRegionsThrough(fresh_var); - - self.setRegionAt(fresh_var, base.Region.zero()); - } - } - - // Assert that we have regions for every type variable - self.debugAssertArraysInSync(); - - return copied_var; -} - // regions // /// Fill slots in the regions array up to and including the target var @@ -385,463 +606,2152 @@ fn fillInRegionsThrough(self: *Self, target_var: Var) Allocator.Error!void { } } -/// The the region for a variable +/// Set the region for a var fn setRegionAt(self: *Self, target_var: Var, new_region: Region) void { self.regions.set(@enumFromInt(@intFromEnum(target_var)), new_region); } +/// Get the region for a var +fn getRegionAt(self: *Self, target_var: Var) Region { + return self.regions.get(@enumFromInt(@intFromEnum(target_var))).*; +} + // fresh vars // -/// The the region for a variable -fn fresh(self: *Self, new_region: Region) Allocator.Error!Var { - const var_ = try self.types.fresh(); +/// Create fresh flex var +fn fresh(self: *Self, env: *Env, new_region: Region) Allocator.Error!Var { + return self.freshFromContent(.{ .flex = Flex.init() }, env, new_region); +} + +/// Create fresh var with the provided content +fn freshFromContent(self: *Self, content: Content, env: *Env, new_region: Region) Allocator.Error!Var { + const var_ = try self.types.freshFromContentWithRank(content, env.rank()); try self.fillInRegionsThrough(var_); self.setRegionAt(var_, new_region); + try env.var_pool.addVarToRank(var_, env.rank()); return var_; } -/// The the region for a variable -fn freshFromContent(self: *Self, content: Content, new_region: Region) Allocator.Error!Var { - const var_ = try self.types.freshFromContent(content); - try self.fillInRegionsThrough(var_); - self.setRegionAt(var_, new_region); - return var_; +/// Create a bool var +fn freshBool(self: *Self, env: *Env, new_region: Region) Allocator.Error!Var { + // Use the copied Bool type from the type store (set by copyBuiltinTypes) + return try self.instantiateVar(self.bool_var, env, .{ .explicit = new_region }); } -// external types // +/// Create a str var +fn freshStr(self: *Self, env: *Env, new_region: Region) Allocator.Error!Var { + // Use the copied Str type from the type store (set by copyBuiltinTypes) + return try self.instantiateVar(self.str_var, env, .{ .explicit = new_region }); +} -const ExternalType = struct { - local_var: Var, - other_cir_node_idx: CIR.Node.Idx, - other_cir: *ModuleEnv, -}; +/// Create a nominal List type with the given element type +fn mkListContent(self: *Self, elem_var: Var, env: *Env) Allocator.Error!Content { + // Use the cached builtin_module_ident from the current module's ident store. + // This represents the "Builtin" module where List is defined. + const origin_module_id = if (self.builtin_ctx.builtin_module) |_| + self.cir.idents.builtin_module + else + self.builtin_ctx.module_name; // We're compiling Builtin module itself -/// Copy a variable from a different module into this module's types store. -/// -/// IMPORTANT: The caller must instantiate this variable before unifing -/// against it. This avoid poisoning the copied variable in the types store if -/// unification fails. -fn resolveVarFromExternal( + const list_ident = types_mod.TypeIdent{ + .ident_idx = self.cir.idents.list, + }; + + // List's backing is [ProvidedByCompiler] with closed extension + // The element type is a type parameter, not the backing + const empty_tag_union_content = Content{ .structure = .empty_tag_union }; + const ext_var = try self.freshFromContent(empty_tag_union_content, env, Region.zero()); + + // Create the [ProvidedByCompiler] tag + const provided_tag_ident = try @constCast(self.cir).insertIdent(base.Ident.for_text("ProvidedByCompiler")); + const provided_tag = try self.types.mkTag(provided_tag_ident, &.{}); + + const tag_union = types_mod.TagUnion{ + .tags = try self.types.appendTags(&[_]types_mod.Tag{provided_tag}), + .ext = ext_var, + }; + const backing_content = Content{ .structure = .{ .tag_union = tag_union } }; + const backing_var = try self.freshFromContent(backing_content, env, Region.zero()); + + const type_args = [_]Var{elem_var}; + + return try self.types.mkNominal( + list_ident, + backing_var, + &type_args, + origin_module_id, + false, // List is nominal (not opaque) + ); +} + +/// Create a nominal number type content (e.g., U8, I32, Dec) +/// Number types are defined in Builtin.roc nested inside Num module: Num.U8 :: [].{...} +/// They have no type parameters and their backing is the empty tag union [] +fn mkNumberTypeContent(self: *Self, type_name: []const u8, env: *Env) Allocator.Error!Content { + const origin_module_id = if (self.builtin_ctx.builtin_module) |_| + self.cir.idents.builtin_module + else + self.builtin_ctx.module_name; // We're compiling Builtin module itself + + // Use fully-qualified type name "Builtin.Num.U8" etc. + // This allows method lookup to work correctly (getMethodIdent builds "Builtin.Num.U8.method_name") + const qualified_type_name = try std.fmt.allocPrint(self.gpa, "Builtin.Num.{s}", .{type_name}); + defer self.gpa.free(qualified_type_name); + const type_name_ident = try @constCast(self.cir).insertIdent(base.Ident.for_text(qualified_type_name)); + const type_ident = types_mod.TypeIdent{ + .ident_idx = type_name_ident, + }; + + // Number types backing is [] (empty tag union with closed extension) + const empty_tag_union_content = Content{ .structure = .empty_tag_union }; + const ext_var = try self.freshFromContent(empty_tag_union_content, env, Region.zero()); + const empty_tag_union = types_mod.TagUnion{ + .tags = types_mod.Tag.SafeMultiList.Range.empty(), + .ext = ext_var, + }; + const backing_content = Content{ .structure = .{ .tag_union = empty_tag_union } }; + const backing_var = try self.freshFromContent(backing_content, env, Region.zero()); + + // Number types have no type arguments + const no_type_args: []const Var = &.{}; + + return try self.types.mkNominal( + type_ident, + backing_var, + no_type_args, + origin_module_id, + true, // Number types are opaque (defined with ::) + ); +} + +/// Create a flex variable with a from_numeral constraint for numeric literals. +/// This constraint will be checked during deferred constraint checking to validate +/// that the numeric literal can be converted to the unified type. +/// Returns the flex var which has the constraint attached, and the dispatcher var +/// (first arg of from_numeral) is unified with the flex var so they share the same name. +fn mkFlexWithFromNumeralConstraint( self: *Self, - module_idx: CIR.Import.Idx, - node_idx: u16, -) std.mem.Allocator.Error!?ExternalType { - const module_idx_int = @intFromEnum(module_idx); - if (module_idx_int < self.other_modules.len) { - const other_module_cir = self.other_modules[module_idx_int]; - const other_module_env = other_module_cir; + num_literal_info: types_mod.NumeralInfo, + env: *Env, +) !Var { + const from_numeral_ident = self.cir.idents.from_numeral; - // The idx of the expression in the other module - const target_node_idx = @as(CIR.Node.Idx, @enumFromInt(node_idx)); + // Create the flex var first - this represents the target type `a` + const flex_var = try self.fresh(env, num_literal_info.region); - // Check if we've already copied this import - const cache_key = ImportCacheKey{ - .module_idx = module_idx, - .node_idx = target_node_idx, - }; + // Create the argument type: Numeral (from Builtin.Num.Numeral) + // For from_numeral, the actual method signature is: Numeral -> Try(a, [InvalidNumeral(Str)]) + const numeral_content = try self.mkNumeralContent(env); + const arg_var = try self.freshFromContent(numeral_content, env, num_literal_info.region); - const copied_var = if (self.import_cache.get(cache_key)) |cached_var| - // Reuse the previously copied type. - cached_var - else blk: { - // First time importing this type - copy it and cache the result - const imported_var = @as(Var, @enumFromInt(@intFromEnum(target_node_idx))); - const new_copy = try self.copyVar(imported_var, other_module_env); - try self.import_cache.put(self.gpa, cache_key, new_copy); - break :blk new_copy; - }; + // Create the error type: [InvalidNumeral(Str)] (closed tag union) + const str_var = self.str_var; + const invalid_numeral_tag_ident = try @constCast(self.cir).insertIdent( + base.Ident.for_text("InvalidNumeral"), + ); + const invalid_numeral_tag = try self.types.mkTag( + invalid_numeral_tag_ident, + &.{str_var}, + ); + // Use empty_tag_union as extension to create a closed tag union [InvalidNumeral(Str)] + const err_ext_var = try self.freshFromContent(.{ .structure = .empty_tag_union }, env, num_literal_info.region); + const err_type = try self.types.mkTagUnion(&.{invalid_numeral_tag}, err_ext_var); + const err_var = try self.freshFromContent(err_type, env, num_literal_info.region); - return .{ - .local_var = copied_var, - .other_cir_node_idx = target_node_idx, - .other_cir = other_module_env, - }; + // Create Try(flex_var, err_var) as the return type + // Try is a nominal type with two type args: the success type and the error type + const try_type_content = try self.mkTryContent(flex_var, err_var); + const ret_var = try self.freshFromContent(try_type_content, env, num_literal_info.region); + + const func_content = types_mod.Content{ + .structure = types_mod.FlatType{ + .fn_unbound = types_mod.Func{ + .args = try self.types.appendVars(&.{arg_var}), + .ret = ret_var, + .needs_instantiation = false, + }, + }, + }; + const fn_var = try self.freshFromContent(func_content, env, num_literal_info.region); + + // Create the constraint with numeric literal info + const constraint = types_mod.StaticDispatchConstraint{ + .fn_name = from_numeral_ident, + .fn_var = fn_var, + .origin = .from_numeral, + .num_literal = num_literal_info, + }; + + // Store it in the types store + const constraint_range = try self.types.appendStaticDispatchConstraints(&.{constraint}); + + // Update the flex var to have the constraint attached + const flex_content = types_mod.Content{ + .flex = types_mod.Flex{ + .name = null, + .constraints = constraint_range, + }, + }; + try self.unifyWith(flex_var, flex_content, env); + + return flex_var; +} + +/// Create a nominal Box type with the given element type +fn mkBoxContent(self: *Self, elem_var: Var) Allocator.Error!Content { + // Use the cached builtin_module_ident from the current module's ident store. + // This represents the "Builtin" module where Box is defined. + const origin_module_id = if (self.builtin_ctx.builtin_module) |_| + self.cir.idents.builtin_module + else + self.builtin_ctx.module_name; // We're compiling Builtin module itself + + const box_ident = types_mod.TypeIdent{ + .ident_idx = self.cir.idents.box, + }; + + // The backing var is the element type var + const backing_var = elem_var; + const type_args = [_]Var{elem_var}; + + return try self.types.mkNominal( + box_ident, + backing_var, + &type_args, + origin_module_id, + false, // Box is nominal (not opaque) + ); +} + +/// Create a nominal Try type with the given success and error types +fn mkTryContent(self: *Self, ok_var: Var, err_var: Var) Allocator.Error!Content { + // Use the cached builtin_module_ident from the current module's ident store. + // This represents the "Builtin" module where Try is defined. + const origin_module_id = if (self.builtin_ctx.builtin_module) |_| + self.cir.idents.builtin_module + else + self.builtin_ctx.module_name; // We're compiling Builtin module itself + + // Use the relative name "Try" (not "Builtin.Try") to match the relative_name in TypeHeader + // The origin_module field already captures that this type is from Builtin + const try_ident = types_mod.TypeIdent{ + .ident_idx = self.cir.idents.builtin_try, + }; + + // The backing var doesn't matter here. Nominal types unify based on their ident + // and type args only - the backing is never examined during unification. + // Creating the real backing type ([Ok(ok), Err(err)]) would be a waste of time. + const backing_var = ok_var; + const type_args = [_]Var{ ok_var, err_var }; + + return try self.types.mkNominal( + try_ident, + backing_var, + &type_args, + origin_module_id, + false, // Try is nominal (not opaque) + ); +} + +/// Create a nominal Numeral type (from Builtin.Num.Numeral) +/// Numeral has no type parameters - it's a concrete record type wrapped in Self tag +fn mkNumeralContent(self: *Self, env: *Env) Allocator.Error!Content { + // Use the cached builtin_module_ident from the current module's ident store. + // This represents the "Builtin" module where Numeral is defined. + const origin_module_id = if (self.builtin_ctx.builtin_module) |_| + self.cir.idents.builtin_module + else + self.builtin_ctx.module_name; // We're compiling Builtin module itself + + // Use the relative name "Num.Numeral" with origin_module Builtin + // Use the pre-interned ident from builtin_module to avoid string comparison + const numeral_ident = types_mod.TypeIdent{ + .ident_idx = self.cir.idents.builtin_numeral, + }; + + // The backing var doesn't matter here. Nominal types unify based on their ident + // and type args only - the backing is never examined during unification. + // Creating the real backing type ([Self({is_negative: Bool, ...})]) would be a waste of time. + const empty_tag_union_content = Content{ .structure = .empty_tag_union }; + const ext_var = try self.freshFromContent(empty_tag_union_content, env, Region.zero()); + const empty_tag_union = types_mod.TagUnion{ + .tags = types_mod.Tag.SafeMultiList.Range.empty(), + .ext = ext_var, + }; + const backing_content = Content{ .structure = .{ .tag_union = empty_tag_union } }; + const backing_var = try self.freshFromContent(backing_content, env, Region.zero()); + + return try self.types.mkNominal( + numeral_ident, + backing_var, + &.{}, // No type args + origin_module_id, + true, // Numeral is opaque (defined with ::) + ); +} + +// updating vars // + +/// Unify the provided variable with the provided content +/// +/// If the var is a flex at the current rank, skip unifcation and simply update +/// the type descriptor +/// +/// This should primarily be use to set CIR node vars that were initially filled with placeholders +fn unifyWith(self: *Self, target_var: Var, content: types_mod.Content, env: *Env) std.mem.Allocator.Error!void { + const resolved_target = self.types.resolveVar(target_var); + if (resolved_target.is_root and resolved_target.desc.rank == env.rank() and resolved_target.desc.content == .flex) { + // The vast majority of the time, we call unify with on a placeholder + // CIR var. In this case, we can safely override the type descriptor + // directly, saving a typeslot and unifcation run + var desc = resolved_target.desc; + desc.content = content; + try self.types.dangerousSetVarDesc(target_var, desc); } else { - return null; + const fresh_var = try self.freshFromContent(content, env, self.getRegionAt(target_var)); + if (builtin.mode == .Debug) { + const target_var_rank = self.types.resolveVar(target_var).desc.rank; + const fresh_var_rank = self.types.resolveVar(fresh_var).desc.rank; + if (@intFromEnum(target_var_rank) > @intFromEnum(fresh_var_rank)) { + std.debug.panic("trying unifyWith unexpected ranks {} & {}", .{ @intFromEnum(target_var_rank), @intFromEnum(fresh_var_rank) }); + } + } + _ = try self.unify(target_var, fresh_var, env); + } +} + +/// Give a var, ensure it's not a redirect and set its rank. +/// If the var is already a redirect, this is a no-op - the root's rank was set when +/// the redirect was created during unification. This can happen when a variable is +/// unified with another before its rank is explicitly set, which is benign. +fn setVarRank(self: *Self, target_var: Var, env: *Env) std.mem.Allocator.Error!void { + const resolved = self.types.resolveVar(target_var); + if (resolved.is_root) { + self.types.setDescRank(resolved.desc_idx, env.rank()); + try env.var_pool.addVarToRank(target_var, env.rank()); + } + // If not root, the variable is a redirect - its rank is determined by the root + // variable it points to, which was handled when the redirect was created. +} + +// file // + +/// Check the types for all defs +/// Copy builtin types from their modules into the current module's type store +/// This is necessary because type variables are module-specific - we can't use Vars from +/// other modules directly. The Bool and Try types are used in language constructs like +/// `if` conditions and need to be available in every module's type store. +fn copyBuiltinTypes(self: *Self) !void { + const bool_stmt_idx = self.builtin_ctx.bool_stmt; + const str_stmt_idx = self.builtin_ctx.str_stmt; + + if (self.builtin_ctx.builtin_module) |builtin_env| { + // Copy Bool type from Builtin module using the direct reference + const bool_type_var = ModuleEnv.varFrom(bool_stmt_idx); + self.bool_var = try self.copyVar(bool_type_var, builtin_env, Region.zero()); + + // Copy Str type from Builtin module using the direct reference + const str_type_var = ModuleEnv.varFrom(str_stmt_idx); + self.str_var = try self.copyVar(str_type_var, builtin_env, Region.zero()); + } else { + // If Builtin module reference is null, use the statement from the current module + // This happens when compiling the Builtin module itself + self.bool_var = ModuleEnv.varFrom(bool_stmt_idx); + self.str_var = ModuleEnv.varFrom(str_stmt_idx); + } + + // Try type is accessed via external references, no need to copy it here +} + +/// Check the types for all defs in a file +pub fn checkFile(self: *Self) std.mem.Allocator.Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + // Fill in types store up to the size of CIR nodes + try ensureTypeStoreIsFilled(self); + + // Create a solver env + var env = try self.env_pool.acquire(.generalized); + defer self.env_pool.release(env); + + // Copy builtin types (Bool, Try) into this module's type store + try self.copyBuiltinTypes(); + + // First, iterate over the builtin statements, generating types for each type declaration + const builtin_stmts_slice = self.cir.store.sliceStatements(self.cir.builtin_statements); + for (builtin_stmts_slice) |builtin_stmt_idx| { + // If the statement is a type declaration, then generate the it's type + // The resulting generalized type is saved at the type var slot at `stmt_idx` + try self.generateStmtTypeDeclType(builtin_stmt_idx, &env); + } + + // Process requires_types annotations for platforms + // This ensures the type store has the actual types for platform requirements + try self.processRequiresTypes(&env); + + const stmts_slice = self.cir.store.sliceStatements(self.cir.all_statements); + + // First pass: generate types for each type declaration + for (stmts_slice) |stmt_idx| { + const stmt = self.cir.store.getStatement(stmt_idx); + const stmt_var = ModuleEnv.varFrom(stmt_idx); + try self.setVarRank(stmt_var, &env); + + switch (stmt) { + .s_alias_decl => |alias| { + try self.generateAliasDecl(stmt_idx, stmt_var, alias, &env); + }, + .s_nominal_decl => |nominal| { + try self.generateNominalDecl(stmt_idx, stmt_var, nominal, &env); + }, + .s_runtime_error => { + try self.unifyWith(stmt_var, .err, &env); + }, + .s_type_anno => |type_anno| { + try self.generateStandaloneTypeAnno(stmt_var, type_anno, &env); + }, + else => { + // All other stmt types are invalid at the top level + }, + } + } + + // Next, capture all top level defs + // This is used to support out-of-order defts + const defs_slice = self.cir.store.sliceDefs(self.cir.all_defs); + for (defs_slice) |def_idx| { + const def = self.cir.store.getDef(def_idx); + try self.top_level_ptrns.put(def.pattern, .{ .def_idx = def_idx, .status = .not_processed }); + } + + // Then, iterate over defs again, inferring types + for (defs_slice) |def_idx| { + env.reset(); + try self.checkDef(def_idx, &env); + } + + // Finally, type-check top-level statements (like expect) + // These are separate from defs and need to be checked after all defs are processed + // so that lookups can find their definitions + for (stmts_slice) |stmt_idx| { + const stmt = self.cir.store.getStatement(stmt_idx); + const stmt_var = ModuleEnv.varFrom(stmt_idx); + const stmt_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(stmt_idx)); + + switch (stmt) { + .s_expect => |expr_stmt| { + env.reset(); + + // Enter a new rank for this expect + try env.var_pool.pushRank(); + defer env.var_pool.popRank(); + + // Check the body expression + _ = try self.checkExpr(expr_stmt.body, &env, .no_expectation); + const body_var: Var = ModuleEnv.varFrom(expr_stmt.body); + + // Unify with Bool (expects must be bool expressions) + const bool_var = try self.freshBool(&env, stmt_region); + _ = try self.unify(bool_var, body_var, &env); + + // Unify statement var with body var + _ = try self.unify(stmt_var, body_var, &env); + + // Generalize and check deferred constraints + try self.generalizer.generalize(self.gpa, &env.var_pool, env.rank()); + try self.checkDeferredStaticDispatchConstraints(&env); + }, + else => { + // Other statement types are handled elsewhere (type decls, defs, etc.) + }, + } + } + + // Note that we can't use SCCs to determine the order to resolve defs + // because anonymous static dispatch makes function order not knowable + // before type inference + +} + +/// Process the requires_types annotations for platform modules, like: +/// +/// { [Model : model] for main : { init : model, ... } } +/// +/// For each required type, we first process the introduced alias variables: +/// { [Model : model] for main : { init : model, ... } } +/// ^^^^^^^^^^^^^^ +/// Here, we create `model` as a *rigid* var, and a type alias `Model` pointing to +/// that exact rigid var. +/// +/// We create this variable at the `.generalized` rank, but we have special +/// logic in `generateAnnoTypeInPlace` so places that reference `Model` +/// directly reference the underlying *uninstantiated* rigid var +/// +/// Then, we generate the type for the actual required type +/// { [Model : model] for main : { init : model, ... } } +/// ^^^^^^^^^^^^^^^^^^^^^^ +/// +/// Note on scoping: Type scopes are defined in czer. So in the example above, +/// { [Model : model] for main : { init : model, ... } } +/// a^^^^^ b^^^^^ +/// So `a` get the node CIR.TypeAnno.rigid_var{ .. } +/// So `b` get the node CIR.TypeAnno.rigid_var_lookup{ .ref = } +/// Then, any reference to `b` replaced with `a` in `generateAnnoTypeInPlace`. +fn processRequiresTypes(self: *Self, env: *Env) std.mem.Allocator.Error!void { + // Ensure we are generalized + // This is because we do not want the type checking we do here to be let-polymorphic + std.debug.assert(env.rank() == .generalized); + + const requires_types_slice = self.cir.requires_types.items.items; + for (requires_types_slice) |required_type| { + + // First, processes the required type aliases + // { [Model : model] for main : { init : model, ... } } + // ^^^^^^^^^^^^^^ + const required_type_aliases_slice = self.cir.for_clause_aliases.sliceRange(required_type.type_aliases); + for (required_type_aliases_slice) |type_alias| { + const stmt = self.cir.store.getStatement(type_alias.alias_stmt_idx); + const stmt_var = ModuleEnv.varFrom(type_alias.alias_stmt_idx); + + // We should only ever have alias decls here + std.debug.assert(stmt == .s_alias_decl); + const alias = stmt.s_alias_decl; + + // Assert that this alias header is well formed + const alias_lhs = self.cir.store.getTypeHeader(alias.header); + std.debug.assert(alias_lhs.name == alias_lhs.relative_name); + std.debug.assert(alias_lhs.args.span.len == 0); + + // Assert that this alias body is well formed + const alias_rhs_var = ModuleEnv.varFrom(alias.anno); + const alias_rhs = self.cir.store.getTypeAnno(alias.anno); + std.debug.assert(alias_rhs == .rigid_var); + + // Set ranks to generalized + try self.setVarRank(stmt_var, env); + try self.setVarRank(alias_rhs_var, env); + + // Set the rhs of the expr to be a rigid var + try self.unifyWith(alias_rhs_var, .{ + .rigid = Rigid.init(type_alias.rigid_name), + }, env); + + // IMPORTANT! + // We *do not* create a real alias here. Instead, we unify the alias + // stmt directly with the backing variable not the alias wrapper, + // so that it can be substituted with the app's concrete type during + // checkPlatformRequirements. + _ = try self.unify(stmt_var, alias_rhs_var, env); + } + + // Then, generate the type for the actual required type + // { [Model : model] for main : { init : model, ... } } + // ^^^^^^^^^^^^^^^^^^^^^^ + try self.generateAnnoTypeInPlace(required_type.type_anno, env, .annotation); + } +} + +/// Check that the app's exported values match the platform's required types. +/// +/// This should be called after checkFile() to verify that app exports conform +/// to the platform's requirements. +/// +/// The `platform_to_app_idents` map translates platform ident indices to app ident indices, +/// built by the caller to avoid string lookups during type checking. +/// +/// TODO: There are some non-type errors that this function produces (like +/// if the required alias or definition) are not found These errors could be +/// reporter in czer. +pub fn checkPlatformRequirements( + self: *Self, + platform_env: *const ModuleEnv, + platform_to_app_idents: *const std.AutoHashMap(Ident.Idx, Ident.Idx), +) std.mem.Allocator.Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + // Create a solver env for type operations + var env = try self.env_pool.acquire(.generalized); + defer self.env_pool.release(env); + + // Iterate over the platform's required types + const requires_types_slice = platform_env.requires_types.items.items; + for (requires_types_slice) |required_type| { + // Look up the pre-translated app ident for this platform requirement + const app_required_ident = platform_to_app_idents.get(required_type.ident); + + // Find the matching export in the app + const app_exports_slice = self.cir.store.sliceDefs(self.cir.exports); + var found_export: ?CIR.Def.Idx = null; + + for (app_exports_slice) |def_idx| { + const def = self.cir.store.getDef(def_idx); + const pattern = self.cir.store.getPattern(def.pattern); + + if (pattern == .assign) { + // Compare ident indices - if app_required_ident is null, there's no match + if (app_required_ident != null and pattern.assign.ident == app_required_ident.?) { + found_export = def_idx; + break; + } + } + } + + if (found_export) |export_def_idx| { + // Get the app export's type variable + const export_def = self.cir.store.getDef(export_def_idx); + const export_var = ModuleEnv.varFrom(export_def.pattern); + + // Copy the required type from the platform's type store into the app's type store + // First, convert the type annotation to a type variable in the platform's context + const required_type_var = ModuleEnv.varFrom(required_type.type_anno); + + // Copy the type from the platform's type store + const copied_required_var = try self.copyVar(required_type_var, platform_env, required_type.region); + + // Instantiate the copied variable before unifying (to avoid poisoning the cached copy) + // IMPORTANT: When we instantiate this rigid here, it is instantiated as a flex + const instantiated_required_var = try self.instantiateVar(copied_required_var, &env, .{ .explicit = required_type.region }); + + // Get the type aliases (eg [Model : model]) for this required type + const type_aliases_range = required_type.type_aliases; + const all_aliases = platform_env.for_clause_aliases.items.items; + const type_aliases_slice = all_aliases[@intFromEnum(type_aliases_range.start)..][0..type_aliases_range.count]; + + // Extract flex name -> instantiated var mappings from the var_map. + var var_map_iter = self.var_map.iterator(); + while (var_map_iter.next()) |entry| { + const fresh_var = entry.value_ptr.*; + const resolved = self.types.resolveVar(fresh_var); + switch (resolved.desc.content) { + // Note that here we match on a flex var. Because the + // type is instantiated any rigid in the platform + // required type become flex + .flex => |flex| { + // Assert flex has name (flex var should come from platform rigid vars) + std.debug.assert(flex.name != null); + const flex_name = flex.name.?; + + // Assert that this flex var ident is in the list of + // rigid vars declared by the platform. + if (builtin.mode == .Debug) { + var found_in_required_aliases = false; + for (type_aliases_slice) |alias| { + const app_rigid_name = platform_to_app_idents.get(alias.rigid_name) orelse continue; + if (app_rigid_name == flex_name) { + found_in_required_aliases = true; + break; + } + } + if (!found_in_required_aliases) { + std.debug.panic("Expected type var with name {s} to be declared in platform required type aliases", .{ + self.cir.getIdentText(flex_name), + }); + } + } + + // Store the rigid (now instantiated flex) name -> instantiated var mapping in the app's module env + try self.cir.rigid_vars.put(self.gpa, flex_name, fresh_var); + }, + else => {}, + } + } + + // For each for-clause type alias (e.g., [Model : model]), look up the app's + // corresponding type alias and unify it with the rigid type variable. + // This substitutes concrete app types for platform rigid type variables. + for (type_aliases_slice) |alias| { + // Translate the platform's alias name to the app's namespace + const app_alias_name = platform_to_app_idents.get(alias.alias_name) orelse { + const expected_alias_ident = try self.cir.insertIdent( + Ident.for_text(platform_env.getIdentText(alias.alias_name)), + ); + _ = try self.problems.appendProblem(self.gpa, .{ .platform_alias_not_found = .{ + .expected_alias_ident = expected_alias_ident, + .ctx = .not_found, + } }); + _ = try self.unifyWith(instantiated_required_var, .err, &env); + _ = try self.unifyWith(export_var, .err, &env); + return; + }; + + // Look up the rigid var we stored earlier. + // rigid_vars is keyed by the APP's ident index (the rigid name was translated when copied), + // so we translate the platform's rigid_name to the app's ident space using the pre-built map. + const app_rigid_name = platform_to_app_idents.get(alias.rigid_name) orelse { + if (builtin.mode == .Debug) { + std.debug.panic("Expected to find platform alias rigid var ident {s} in module", .{ + platform_env.getIdentText(alias.rigid_name), + }); + } + _ = try self.unifyWith(instantiated_required_var, .err, &env); + _ = try self.unifyWith(export_var, .err, &env); + return; + }; + const rigid_var = self.cir.rigid_vars.get(app_rigid_name) orelse { + if (builtin.mode == .Debug) { + std.debug.panic("Expected to find rigid var in map {s} in instantiate platform required type", .{ + platform_env.getIdentText(alias.rigid_name), + }); + } + _ = try self.unifyWith(instantiated_required_var, .err, &env); + _ = try self.unifyWith(export_var, .err, &env); + return; + }; + + // Look up the app's type alias's (eg Model) body (the underlying type, not the alias wrapper) + const app_type_var = self.findTypeAliasBodyVar(app_alias_name) orelse { + const expected_alias_ident = try self.cir.insertIdent( + Ident.for_text(platform_env.getIdentText(alias.alias_name)), + ); + _ = try self.problems.appendProblem(self.gpa, .{ .platform_alias_not_found = .{ + .expected_alias_ident = expected_alias_ident, + .ctx = .found_but_not_alias, + } }); + _ = try self.unifyWith(instantiated_required_var, .err, &env); + _ = try self.unifyWith(export_var, .err, &env); + return; + }; + + // Now unify the (now-flex) var with the app's type alias body. + // This properly handles rank propagation (unlike dangerousSetVarRedirect). + _ = try self.unify(rigid_var, app_type_var, &env); + } + + // Unify the platform's required type with the app's export type. + // This constrains type variables in the export (e.g., closure params) + // to match the platform's expected types. After this, the fresh vars + // stored in rigid_vars will redirect to the concrete app types. + _ = try self.unifyFromAnno(instantiated_required_var, export_var, &env); + } else { + // If we got here, it means that the the definition was not found in + // the module's *export* list + const expected_def_ident = try self.cir.insertIdent( + Ident.for_text(platform_env.getIdentText(required_type.ident)), + ); + _ = try self.problems.appendProblem(self.gpa, .{ + .platform_def_not_found = .{ + .expected_def_ident = expected_def_ident, + .ctx = blk: { + // We know the def is not exported, but here we check + // if it's defined *but not exported* in the module so + // we can show a nicer error message + + var found_def: ?CIR.Def.Idx = null; + + // Check all defs in the module + const app_defs_slice = self.cir.store.sliceDefs(self.cir.all_defs); + for (app_defs_slice) |def_idx| { + const def = self.cir.store.getDef(def_idx); + const pattern = self.cir.store.getPattern(def.pattern); + + if (pattern == .assign) { + // Compare ident indices - if app_required_ident is null, there's no match + if (app_required_ident != null and pattern.assign.ident == app_required_ident.?) { + found_def = def_idx; + break; + } + } + } + + // Break with more specific context + if (found_def == null) { + break :blk .not_found; + } else { + break :blk .found_but_not_exported; + } + }, + }, + }); + } + // Note: If the export is not found, the canonicalizer should have already reported an error + } +} + +/// Find a type alias declaration by name and return the var for its underlying type. +/// This returns the var for the alias's body (e.g., for `Model : { value: I64 }` returns the var for `{ value: I64 }`), +/// not the var for the alias declaration itself. +/// Returns null if no type alias declaration with the given name is found. +fn findTypeAliasBodyVar(self: *Self, name: Ident.Idx) ?Var { + const stmts_slice = self.cir.store.sliceStatements(self.cir.all_statements); + for (stmts_slice) |stmt_idx| { + const stmt = self.cir.store.getStatement(stmt_idx); + switch (stmt) { + .s_alias_decl => |alias| { + const header = self.cir.store.getTypeHeader(alias.header); + if (header.relative_name == name) { + // Return the var for the alias body annotation, not the statement + return ModuleEnv.varFrom(alias.anno); + } + }, + else => {}, + } + } + return null; +} + +/// Check if a statement index is a for-clause alias statement. +/// For-clause alias statements are created during platform header processing +/// for type aliases like [Model : model] in the requires clause. +/// +/// When these are looked up, we need to *not* instantiate the alias, so all +/// references in the module Point to the same var. +fn isForClauseAliasStatement(self: *Self, stmt_idx: CIR.Statement.Idx) bool { + // Slice the for-clause alias statements and check if stmt_idx is in the list + for (self.cir.for_clause_aliases.items.items) |for_clause| { + if (stmt_idx == for_clause.alias_stmt_idx) { + return true; + } + } + return false; +} + +// repl // + +/// Check an expr for the repl +pub fn checkExprRepl(self: *Self, expr_idx: CIR.Expr.Idx) std.mem.Allocator.Error!void { + try ensureTypeStoreIsFilled(self); + + // Copy builtin types into this module's type store + try self.copyBuiltinTypes(); + + // Create a solver env + var env = try self.env_pool.acquire(.generalized); + defer self.env_pool.release(env); + + // First, iterate over the statements, generating types for each type declaration + const stms_slice = self.cir.store.sliceStatements(self.cir.builtin_statements); + for (stms_slice) |stmt_idx| { + // If the statement is a type declaration, then generate the it's type + // The resulting generalized type is saved at the type var slot at `stmt_idx` + try self.generateStmtTypeDeclType(stmt_idx, &env); + } + + { + try env.var_pool.pushRank(); + defer env.var_pool.popRank(); + + // Check the expr + _ = try self.checkExpr(expr_idx, &env, .no_expectation); + + // Now that we are existing the scope, we must generalize then pop this rank + try self.generalizer.generalize(self.gpa, &env.var_pool, env.rank()); + + // Check any accumulated static dispatch constraints + try self.checkDeferredStaticDispatchConstraints(&env); } } // defs // -/// Check the types for all defs -pub fn checkDefs(self: *Self) std.mem.Allocator.Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - const defs_slice = self.cir.store.sliceDefs(self.cir.all_defs); - for (defs_slice) |def_idx| { - try self.checkDef(def_idx); - } -} - /// Check the types for a single definition -fn checkDef(self: *Self, def_idx: CIR.Def.Idx) std.mem.Allocator.Error!void { +fn checkDef(self: *Self, def_idx: CIR.Def.Idx, env: *Env) std.mem.Allocator.Error!void { const trace = tracy.trace(@src()); defer trace.end(); + // Ensure that initiailly we're at the generalized level + std.debug.assert(env.rank() == .generalized); + const def = self.cir.store.getDef(def_idx); - const expr_var: Var = ModuleEnv.varFrom(def.expr); - const expr_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(def.expr)); - - // Check the pattern - try self.checkPattern(def.pattern); - - // Get the defs var slot const def_var = ModuleEnv.varFrom(def_idx); + const ptrn_var = ModuleEnv.varFrom(def.pattern); + const expr_var = ModuleEnv.varFrom(def.expr); - // Handle if there's an annotation associated with this def - if (def.annotation) |anno_idx| { - const annotation = self.cir.store.getAnnotation(anno_idx); - const expr = self.cir.store.getExpr(def.expr); - - const anno_var = ModuleEnv.varFrom(annotation.type_anno); - self.annotation_rigid_var_subs.items.clearRetainingCapacity(); - try self.checkAnnotation(annotation.type_anno); - - if (expr == .e_lambda) { - // Special handling for lambda expressions with annotations - _ = try self.checkLambdaWithAnno( - def.expr, - expr_region, - expr.e_lambda, - anno_var, - ); - } else if (expr == .e_closure) { - // For closures with annotations, we need special handling - const closure = expr.e_closure; - const lambda_expr = self.cir.store.getExpr(closure.lambda_idx); - - if (lambda_expr == .e_lambda) { - // Use special lambda handling to propagate constraints without changing error locations - _ = try self.checkLambdaForClosure( - closure.lambda_idx, - lambda_expr.e_lambda, - anno_var, - ); - // Unify closure with lambda - _ = try self.unify(expr_var, ModuleEnv.varFrom(closure.lambda_idx)); - // Now unify with annotation for final validation - _ = try self.unifyWithAnnotation(expr_var, anno_var); - } else { - // Shouldn't happen - closures should contain lambdas - _ = try self.checkExpr(def.expr); - _ = try self.unifyWithAnnotation(expr_var, anno_var); - } - } else { - // Check the expr - _ = try self.checkExpr(def.expr); - - // Unify the expression with its annotation - _ = try self.unifyWithAnnotation(expr_var, anno_var); + if (self.top_level_ptrns.get(def.pattern)) |processing_def| { + if (processing_def.status == .processed) { + // If we've already processed this def, return immediately + return; } - } else { - // Check the expr - _ = try self.checkExpr(def.expr); } - // Unify the def with its expression - _ = try self.unify(def_var, ModuleEnv.varFrom(def.expr)); + // Make as processing + try self.top_level_ptrns.put(def.pattern, .{ .def_idx = def_idx, .status = .processing }); - // Also unify the pattern with the def - needed so lookups work correctly - // TODO could we unify directly with the pattern elsewhere, to save a type var and unify() here? - _ = try self.unify(ModuleEnv.varFrom(def.pattern), def_var); + { + try env.var_pool.pushRank(); + defer env.var_pool.popRank(); + + std.debug.assert(env.rank() == .top_level); + + try self.setVarRank(def_var, env); + try self.setVarRank(ptrn_var, env); + try self.setVarRank(expr_var, env); + + // Check the pattern + try self.checkPattern(def.pattern, env, .no_expectation); + + // Extract function name from the pattern (for better error messages) + const saved_func_name = self.enclosing_func_name; + self.enclosing_func_name = blk: { + const pattern = self.cir.store.getPattern(def.pattern); + switch (pattern) { + .assign => |assign| break :blk assign.ident, + else => break :blk null, + } + }; + defer self.enclosing_func_name = saved_func_name; + + // Handle if there's an annotation associated with this def + if (def.annotation) |annotation_idx| { + // Generate the annotation type + self.anno_free_vars.items.clearRetainingCapacity(); + try self.generateAnnotationType(annotation_idx, env); + const annotation_var = ModuleEnv.varFrom(annotation_idx); + + // TODO: If we instantiate here, then var lookups break. But if we don't + // then the type anno gets corrupted if we have an error in the body + // const instantiated_anno_var = try self.instantiateVarPreserveRigids( + // annotation_var, + // rank, + // .use_last_var, + // ); + + // Infer types for the body, checking against the instantaited annotation + _ = try self.checkExpr(def.expr, env, .{ + .expected = .{ .var_ = annotation_var, .from_annotation = true }, + }); + + // Check that the annotation matches the definition + _ = try self.unify(annotation_var, def_var, env); + } else { + // Check the expr + _ = try self.checkExpr(def.expr, env, .no_expectation); + } + + // Now that we are exiting the scope, we must generalize then pop this rank + try self.generalizer.generalize(self.gpa, &env.var_pool, env.rank()); + + // Check any accumulated static dispatch constraints + try self.checkDeferredStaticDispatchConstraints(env); + + // Check that the ptrn and the expr match + _ = try self.unify(ptrn_var, expr_var, env); + + // Check that the def and ptrn match + _ = try self.unify(def_var, ptrn_var, env); + } + + // Mark as processed + try self.top_level_ptrns.put(def.pattern, .{ .def_idx = def_idx, .status = .processed }); } +// create types for type decls // + +/// Generate a type variable from the provided type statement. +/// If the stmt is not an alias or nominal dec, then do nothing +/// +/// The created variable is put in-place at the var slot at `decl_idx` +/// The created variable will be generalized +fn generateStmtTypeDeclType( + self: *Self, + decl_idx: CIR.Statement.Idx, + env: *Env, +) std.mem.Allocator.Error!void { + const decl_free_vars_top = self.decl_free_vars.top(); + defer self.decl_free_vars.clearFrom(decl_free_vars_top); + + const decl = self.cir.store.getStatement(decl_idx); + const decl_var = ModuleEnv.varFrom(decl_idx); + + switch (decl) { + .s_alias_decl => |alias| { + try self.generateAliasDecl(decl_idx, decl_var, alias, env); + }, + .s_nominal_decl => |nominal| { + try self.generateNominalDecl(decl_idx, decl_var, nominal, env); + }, + .s_runtime_error => { + try self.unifyWith(decl_var, .err, env); + }, + else => { + // Do nothing + }, + } +} + +/// Generate types for an alias type declaration +fn generateAliasDecl( + self: *Self, + decl_idx: CIR.Statement.Idx, + decl_var: Var, + alias: std.meta.fieldInfo(CIR.Statement, .s_alias_decl).type, + env: *Env, +) std.mem.Allocator.Error!void { + // Get the type header's args + const header = self.cir.store.getTypeHeader(alias.header); + const header_args = self.cir.store.sliceTypeAnnos(header.args); + + // Next, generate the provided arg types and build the map of rigid variables in the header + const header_vars = try self.generateHeaderVars(header_args, env); + + // Now we have a built of list of rigid variables for the decl lhs (header). + // With this in hand, we can now generate the type for the lhs (body). + self.seen_annos.clearRetainingCapacity(); + const backing_var: Var = ModuleEnv.varFrom(alias.anno); + try self.generateAnnoTypeInPlace(alias.anno, env, .{ .type_decl = .{ + .idx = decl_idx, + .name = header.relative_name, + .type_ = .alias, + .backing_var = backing_var, + .num_args = @intCast(header_args.len), + } }); + + try self.unifyWith( + decl_var, + try self.types.mkAlias( + .{ .ident_idx = header.relative_name }, + backing_var, + header_vars, + ), + env, + ); +} + +/// Generate types for nominal type declaration +fn generateNominalDecl( + self: *Self, + decl_idx: CIR.Statement.Idx, + decl_var: Var, + nominal: std.meta.fieldInfo(CIR.Statement, .s_nominal_decl).type, + env: *Env, +) std.mem.Allocator.Error!void { + // Get the type header's args + const header = self.cir.store.getTypeHeader(nominal.header); + const header_args = self.cir.store.sliceTypeAnnos(header.args); + + // Next, generate the provided arg types and build the map of rigid variables in the header + const header_vars = try self.generateHeaderVars(header_args, env); + + // Now we have a built of list of rigid variables for the decl lhs (header). + // With this in hand, we can now generate the type for the lhs (body). + self.seen_annos.clearRetainingCapacity(); + const backing_var: Var = ModuleEnv.varFrom(nominal.anno); + try self.generateAnnoTypeInPlace(nominal.anno, env, .{ .type_decl = .{ + .idx = decl_idx, + .name = header.relative_name, + .type_ = .nominal, + .backing_var = backing_var, + .num_args = @intCast(header_args.len), + } }); + + try self.unifyWith( + decl_var, + try self.types.mkNominal( + .{ .ident_idx = header.relative_name }, + backing_var, + header_vars, + self.builtin_ctx.module_name, + nominal.is_opaque, + ), + env, + ); +} + +/// Generate types for a standalone type annotation (one without a corresponding definition). +/// These are typically used for FFI function declarations or forward declarations. +fn generateStandaloneTypeAnno( + self: *Self, + stmt_var: Var, + type_anno: std.meta.fieldInfo(CIR.Statement, .s_type_anno).type, + env: *Env, +) std.mem.Allocator.Error!void { + // Reset seen type annos + self.seen_annos.clearRetainingCapacity(); + + // Save top of scratch static dispatch constraints + const scratch_static_dispatch_constraints_top = self.scratch_static_dispatch_constraints.top(); + defer self.scratch_static_dispatch_constraints.clearFrom(scratch_static_dispatch_constraints_top); + + // Iterate over where clauses (if they exist), adding them to scratch_static_dispatch_constraints + if (type_anno.where) |where_span| { + const where_slice = self.cir.store.sliceWhereClauses(where_span); + for (where_slice) |where_idx| { + try self.generateStaticDispatchConstraintFromWhere(where_idx, env); + } + } + + // Generate the type from the annotation + const anno_var: Var = ModuleEnv.varFrom(type_anno.anno); + try self.generateAnnoTypeInPlace(type_anno.anno, env, .annotation); + + // Unify the statement variable with the generated annotation type + _ = try self.unify(stmt_var, anno_var, env); +} + +/// Generate types for type anno args +fn generateHeaderVars( + self: *Self, + header_args: []CIR.TypeAnno.Idx, + env: *Env, +) std.mem.Allocator.Error![]Var { + for (header_args) |header_arg_idx| { + const header_arg = self.cir.store.getTypeAnno(header_arg_idx); + const header_var = ModuleEnv.varFrom(header_arg_idx); + try self.setVarRank(header_var, env); + + switch (header_arg) { + .rigid_var => |rigid| { + try self.unifyWith(header_var, .{ .rigid = Rigid.init(rigid.name) }, env); + }, + .underscore, .malformed => { + try self.unifyWith(header_var, .err, env); + }, + else => { + // The canonicalizer should only produce rigid_var, underscore, or malformed + // for header args. If we hit this, there's a compiler bug. + std.debug.assert(false); + try self.unifyWith(header_var, .err, env); + }, + } + } + + return @ptrCast(header_args); +} + +// type gen config // + +const OutVar = enum { + in_place, + fresh, + + pub fn voidOrVar(comptime out_var: OutVar) type { + return switch (out_var) { + .in_place => void, + .fresh => Var, + }; + } +}; + // annotations // -/// Check the types for the provided pattern -pub fn checkAnnotation(self: *Self, anno_idx: CIR.TypeAnno.Idx) std.mem.Allocator.Error!void { +/// The context use for free var generation +const GenTypeAnnoCtx = union(enum) { + annotation, + type_decl: struct { + idx: CIR.Statement.Idx, + name: Ident.Idx, + type_: enum { nominal, alias }, + backing_var: Var, + num_args: u32, + }, +}; + +fn generateAnnotationType(self: *Self, annotation_idx: CIR.Annotation.Idx, env: *Env) std.mem.Allocator.Error!void { const trace = tracy.trace(@src()); defer trace.end(); + const annotation_var = ModuleEnv.varFrom(annotation_idx); + try self.setVarRank(annotation_var, env); + + const annotation = self.cir.store.getAnnotation(annotation_idx); + + // Reset seen type annos + self.seen_annos.clearRetainingCapacity(); + + // Save top of scratch static dispatch constraints + const scratch_static_dispatch_constraints_top = self.scratch_static_dispatch_constraints.top(); + defer self.scratch_static_dispatch_constraints.clearFrom(scratch_static_dispatch_constraints_top); + + // Iterate over where clauses (if they exist), adding them to scratch_static_dispatch_constraints + if (annotation.where) |where_span| { + const where_slice = self.cir.store.sliceWhereClauses(where_span); + for (where_slice) |where_idx| { + try self.generateStaticDispatchConstraintFromWhere(where_idx, env); + } + } + + // Then, generate the type for the annotation + try self.generateAnnoTypeInPlace(annotation.anno, env, .annotation); + + // Redirect the root annotation to inner annotation + _ = try self.unify(annotation_var, ModuleEnv.varFrom(annotation.anno), env); +} + +/// Given a where clause, generate static dispatch constraints and add to scratch_static_dispatch_constraints +fn generateStaticDispatchConstraintFromWhere(self: *Self, where_idx: CIR.WhereClause.Idx, env: *Env) std.mem.Allocator.Error!void { + const where = self.cir.store.getWhereClause(where_idx); + const where_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(where_idx)); + + switch (where) { + .w_method => |method| { + // Generate type of the thing dispatch receiver + try self.generateAnnoTypeInPlace(method.var_, env, .annotation); + const method_var = ModuleEnv.varFrom(method.var_); + + // Generate the arguments + const args_anno_slice = self.cir.store.sliceTypeAnnos(method.args); + for (args_anno_slice) |arg_anno_idx| { + try self.generateAnnoTypeInPlace(arg_anno_idx, env, .annotation); + } + const anno_arg_vars: []Var = @ptrCast(args_anno_slice); + + // Generate return type + try self.generateAnnoTypeInPlace(method.ret, env, .annotation); + const ret_var = ModuleEnv.varFrom(method.ret); + + // Create the function var + const func_content = try self.types.mkFuncUnbound(anno_arg_vars, ret_var); + const func_var = try self.freshFromContent(func_content, env, where_region); + + // Add to scratch list + try self.scratch_static_dispatch_constraints.append(ScratchStaticDispatchConstraint{ + .var_ = method_var, + .constraint = StaticDispatchConstraint{ + .fn_name = method.method_name, + .fn_var = func_var, + .origin = .where_clause, + }, + }); + }, + .w_alias => |alias| { + // Alias syntax in where clauses (e.g., `where [a.Decode]`) was used for abilities, + // which have been removed from Roc. Emit an error. + _ = try self.problems.appendProblem(self.gpa, .{ .unsupported_alias_where_clause = .{ + .alias_name = alias.alias_name, + .region = where_region, + } }); + }, + .w_malformed => { + // If it's malformed, just ignore + }, + } +} + +/// Given an annotation, generate the corresponding type based on the CIR +/// +/// This is used both for generation annotation types and type declaration types +/// +/// This function will write the type into the type var node at `anno_idx` +/// +/// Note on scoping for type decls: Type scopes are defined in czer +/// Point(x) : [Point(x, x)] +/// a^ b^ ^c +/// +/// So `a` get the node CIR.TypeAnno.rigid_var{ .. } +/// And `b` & `c` get the node CIR.TypeAnno.rigid_var_lookup{ .ref = } +/// Then, any reference to `b` or `c` are replaced with `a` in `generateAnnoTypeInPlace`. +fn generateAnnoTypeInPlace(self: *Self, anno_idx: CIR.TypeAnno.Idx, env: *Env, ctx: GenTypeAnnoCtx) std.mem.Allocator.Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + // First, check if we've seen this anno before + // This guards against recursive types + if (self.seen_annos.get(anno_idx)) |_| { + return; + } + + // Get the annotation const anno = self.cir.store.getTypeAnno(anno_idx); - const anno_var = ModuleEnv.varFrom(anno_idx); const anno_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(anno_idx)); + const anno_var = ModuleEnv.varFrom(anno_idx); + try self.setVarRank(anno_var, env); + + // Put this anno in the "seen" map immediately, to support recursive references + try self.seen_annos.put(anno_idx, anno_var); switch (anno) { - .ty_lookup_external => |a| { - const resolved_external = try self.resolveVarFromExternal(a.module_idx, a.target_node_idx) orelse { - // If we could not copy the type, set error and continue - try self.types.setVarContent(ModuleEnv.varFrom(anno_idx), .err); - return; - }; + .rigid_var => |rigid| { + const static_dispatch_constraints_start = self.types.static_dispatch_constraints.len(); + switch (ctx) { + .annotation => { + // If this an annotation, then check all where constraints + // and see if any reference this rigid var + for (self.scratch_static_dispatch_constraints.items.items) |scratch_constraint| { + const resolved_scratch_var = self.types.resolveVar(scratch_constraint.var_).var_; + if (resolved_scratch_var == anno_var) { + _ = try self.types.static_dispatch_constraints.append(self.types.gpa, scratch_constraint.constraint); + } + } + }, + .type_decl => {}, + } + const static_dispatch_constraints_end = self.types.static_dispatch_constraints.len(); + const static_dispatch_constraints_range = StaticDispatchConstraint.SafeList.Range{ .start = @enumFromInt(static_dispatch_constraints_start), .count = @intCast(static_dispatch_constraints_end - static_dispatch_constraints_start) }; - self.annotation_rigid_var_subs.items.clearRetainingCapacity(); - const instantatied_var = try self.instantiateVar( - resolved_external.local_var, - &self.annotation_rigid_var_subs, - .{ .explicit = anno_region }, - ); - try self.types.setVarRedirect(anno_var, instantatied_var); + try self.unifyWith(anno_var, .{ .rigid = Rigid{ + .name = rigid.name, + .constraints = static_dispatch_constraints_range, + } }, env); + }, + .rigid_var_lookup => |rigid_lookup| { + _ = try self.unify(anno_var, ModuleEnv.varFrom(rigid_lookup.ref), env); + }, + .underscore => { + try self.unifyWith(anno_var, .{ .flex = Flex.init() }, env); + }, + .lookup => |lookup| { + switch (lookup.base) { + .builtin => |builtin_type| { + try self.setBuiltinTypeContent(anno_var, lookup.name, builtin_type, &.{}, anno_region, env); + }, + .local => |local| { + + // Check if we're in a declaration or an annotation + switch (ctx) { + .type_decl => |this_decl| { + // If so, check if this is a recursive reference + if (this_decl.idx == local.decl_idx) { + + // If it is a recursive ref, check that there are + // no arguments (since this is a lookup, not an apply) + if (this_decl.num_args != 0) { + _ = try self.problems.appendProblem(self.gpa, .{ .type_apply_mismatch_arities = .{ + .type_name = this_decl.name, + .region = anno_region, + .num_expected_args = this_decl.num_args, + .num_actual_args = 0, + } }); + try self.unifyWith(anno_var, .err, env); + return; + } + + // If so, then update this annotation to be an instance + // of this type using the same backing variable + switch (this_decl.type_) { + .alias => { + // Recursion is not allowed in aliases - emit error + _ = try self.problems.appendProblem(self.gpa, .{ .recursive_alias = .{ + .type_name = this_decl.name, + .region = anno_region, + } }); + try self.unifyWith(anno_var, .err, env); + return; + }, + .nominal => { + // Nominal types can be recursive + try self.unifyWith(anno_var, try self.types.mkNominal( + .{ .ident_idx = this_decl.name }, + this_decl.backing_var, + &.{}, + self.builtin_ctx.module_name, + false, // Default to non-opaque for error case + ), env); + }, + } + + return; + } + }, + .annotation => { + // Otherwise, we're in an annotation and this cannot + // be recursive + }, + } + + const local_decl_var = ModuleEnv.varFrom(local.decl_idx); + + // Check if this is a for-clause alias (eg Model [Model : model]). + // For for-clause aliases, we do not want to instantiate the + // variable - each place that references it should reference + // the same var. + const is_for_clause_alias = self.isForClauseAliasStatement(local.decl_idx); + if (is_for_clause_alias) { + _ = try self.unify(anno_var, local_decl_var, env); + } else { + const instantiated_var = try self.instantiateVar(local_decl_var, env, .{ .explicit = anno_region }); + _ = try self.unify(anno_var, instantiated_var, env); + } + }, + .external => |ext| { + if (try self.resolveVarFromExternal(ext.module_idx, ext.target_node_idx)) |ext_ref| { + const ext_instantiated_var = try self.instantiateVar( + ext_ref.local_var, + env, + .{ .explicit = anno_region }, + ); + _ = try self.unify(anno_var, ext_instantiated_var, env); + } else { + // If this external type is unresolved, can should've reported + // an error. So we set to error and continue + try self.unifyWith(anno_var, .err, env); + } + }, + } }, .apply => |a| { - // For apply annotation, the root anno's var is intitally set to be - // a redirect to backing type declaration. - try self.checkApplyAnno(anno_var, anno_region, a.args); - }, - .apply_external => |a| { - const resolved_external = try self.resolveVarFromExternal(a.module_idx, a.target_node_idx) orelse { - // If we could not copy the type, set error and continue - try self.types.setVarContent(ModuleEnv.varFrom(anno_idx), .err); - return; - }; + // Generate the types for the arguments + const anno_args = self.cir.store.sliceTypeAnnos(a.args); + for (anno_args) |anno_arg| { + try self.generateAnnoTypeInPlace(anno_arg, env, ctx); + } + const anno_arg_vars: []Var = @ptrCast(anno_args); - try self.checkApplyAnno(resolved_external.local_var, anno_region, a.args); - }, - .tag_union => |tag_union| { - const args_anno_slice = self.cir.store.sliceTypeAnnos(tag_union.tags); - for (args_anno_slice) |arg_anno_idx| { - try self.checkAnnotation(arg_anno_idx); - } - }, - .tuple => |tuple| { - const args_anno_slice = self.cir.store.sliceTypeAnnos(tuple.elems); - for (args_anno_slice) |arg_anno_idx| { - try self.checkAnnotation(arg_anno_idx); - } - }, - .record => |rec| { - const recs_anno_slice = self.cir.store.sliceAnnoRecordFields(rec.fields); - for (recs_anno_slice) |rec_anno_idx| { - const rec_field = self.cir.store.getAnnoRecordField(rec_anno_idx); - try self.checkAnnotation(rec_field.ty); + switch (a.base) { + .builtin => |builtin_type| { + try self.setBuiltinTypeContent(anno_var, a.name, builtin_type, anno_arg_vars, anno_region, env); + }, + .local => |local| { + // Check if we're in a declaration or an annotation + switch (ctx) { + .type_decl => |this_decl| { + // If so, check if this is a recursive reference + if (this_decl.idx == local.decl_idx) { + + // If it is a recursive ref, check that the args being + // applied here match the number of args of the decl + if (anno_arg_vars.len != this_decl.num_args) { + _ = try self.problems.appendProblem(self.gpa, .{ .type_apply_mismatch_arities = .{ + .type_name = this_decl.name, + .region = anno_region, + .num_expected_args = this_decl.num_args, + .num_actual_args = @intCast(anno_args.len), + } }); + try self.unifyWith(anno_var, .err, env); + return; + } + + // If so, then update this annotation to be an instance + // of this type using the same backing variable + switch (this_decl.type_) { + .alias => { + // Recursion is not allowed in aliases - emit error + _ = try self.problems.appendProblem(self.gpa, .{ .recursive_alias = .{ + .type_name = this_decl.name, + .region = anno_region, + } }); + try self.unifyWith(anno_var, .err, env); + return; + }, + .nominal => { + // Nominal types can be recursive + try self.unifyWith(anno_var, try self.types.mkNominal( + .{ .ident_idx = this_decl.name }, + this_decl.backing_var, + anno_arg_vars, + self.builtin_ctx.module_name, + false, // Default to non-opaque for error case + ), env); + }, + } + + return; + } + }, + .annotation => { + // Otherwise, we're in an annotation and this cannot + // be recursive + }, + } + + // Resolve the referenced type + const decl_var = ModuleEnv.varFrom(local.decl_idx); + const decl_resolved = self.types.resolveVar(decl_var).desc.content; + + // Get the arguments & name the referenced type + const decl_arg_vars, const decl_name = blk: { + if (decl_resolved == .alias) { + const decl_alias = decl_resolved.alias; + break :blk .{ self.types.sliceAliasArgs(decl_alias), decl_alias.ident.ident_idx }; + } else if (decl_resolved == .structure and decl_resolved.structure == .nominal_type) { + const decl_nominal = decl_resolved.structure.nominal_type; + break :blk .{ self.types.sliceNominalArgs(decl_nominal), decl_nominal.ident.ident_idx }; + } else if (decl_resolved == .err) { + try self.unifyWith(anno_var, .err, env); + return; + } else { + // Type applications should only reference aliases or nominal types. + // If we hit this, there's a compiler bug. + std.debug.assert(false); + try self.unifyWith(anno_var, .err, env); + return; + } + }; + + // Check for an arity mismatch + if (decl_arg_vars.len != anno_arg_vars.len) { + _ = try self.problems.appendProblem(self.gpa, .{ .type_apply_mismatch_arities = .{ + .type_name = decl_name, + .region = anno_region, + .num_expected_args = @intCast(decl_arg_vars.len), + .num_actual_args = @intCast(anno_args.len), + } }); + try self.unifyWith(anno_var, .err, env); + return; + } + + // Then, built the map of applied variables + self.rigid_var_substitutions.clearRetainingCapacity(); + for (decl_arg_vars, anno_arg_vars) |decl_arg_var, anno_arg_var| { + const decl_arg_resolved = self.types.resolveVar(decl_arg_var).desc.content; + + std.debug.assert(decl_arg_resolved == .rigid); + const decl_arg_rigid = decl_arg_resolved.rigid; + + try self.rigid_var_substitutions.put(self.gpa, decl_arg_rigid.name, anno_arg_var); + } + + // Then instantiate the variable, substituting the rigid + // variables in the definition with the applied args from + // the annotation + const instantiated_var = try self.instantiateVarWithSubs( + decl_var, + &self.rigid_var_substitutions, + env, + .{ .explicit = anno_region }, + ); + _ = try self.unify(anno_var, instantiated_var, env); + }, + .external => |ext| { + if (try self.resolveVarFromExternal(ext.module_idx, ext.target_node_idx)) |ext_ref| { + // Resolve the referenced type + const ext_resolved = self.types.resolveVar(ext_ref.local_var).desc.content; + + // Get the arguments & name the referenced type + const ext_arg_vars, const ext_name = blk: { + switch (ext_resolved) { + .alias => |decl_alias| { + break :blk .{ self.types.sliceAliasArgs(decl_alias), decl_alias.ident.ident_idx }; + }, + .structure => |flat_type| { + if (flat_type == .nominal_type) { + const decl_nominal = flat_type.nominal_type; + break :blk .{ self.types.sliceNominalArgs(decl_nominal), decl_nominal.ident.ident_idx }; + } else { + // External type resolved to a non-nominal structure (e.g., record, func, etc.) + // This shouldn't happen for type applications, treat as error + try self.unifyWith(anno_var, .err, env); + return; + } + }, + .err => { + try self.unifyWith(anno_var, .err, env); + return; + }, + .flex, .rigid, .recursion_var => { + // External type resolved to a flex, rigid, or recursion var. + // This can happen when the external type is polymorphic but hasn't been + // instantiated yet. We need to use the variable as-is, but this means + // we can't get the arity/name information. This is likely a bug in how + // the external type was set up. For now, treat it as an error. + try self.unifyWith(anno_var, .err, env); + return; + }, + } + }; + + // Check for an arity mismatch + if (ext_arg_vars.len != anno_arg_vars.len) { + _ = try self.problems.appendProblem(self.gpa, .{ .type_apply_mismatch_arities = .{ + .type_name = ext_name, + .region = anno_region, + .num_expected_args = @intCast(ext_arg_vars.len), + .num_actual_args = @intCast(anno_args.len), + } }); + try self.unifyWith(anno_var, .err, env); + return; + } + + // Then, built the map of applied variables + self.rigid_var_substitutions.clearRetainingCapacity(); + for (ext_arg_vars, anno_arg_vars) |decl_arg_var, anno_arg_var| { + const decl_arg_resolved = self.types.resolveVar(decl_arg_var).desc.content; + + std.debug.assert(decl_arg_resolved == .rigid); + const decl_arg_rigid = decl_arg_resolved.rigid; + + try self.rigid_var_substitutions.put(self.gpa, decl_arg_rigid.name, anno_arg_var); + } + + // Then instantiate the variable, substituting the rigid + // variables in the definition with the applied args from + // the annotation + const instantiated_var = try self.instantiateVarWithSubs( + ext_ref.local_var, + &self.rigid_var_substitutions, + env, + .{ .explicit = anno_region }, + ); + _ = try self.unify(anno_var, instantiated_var, env); + } else { + // If this external type is unresolved, can should've reported + // an error. So we set to error and continue + try self.unifyWith(anno_var, .err, env); + } + }, } }, .@"fn" => |func| { const args_anno_slice = self.cir.store.sliceTypeAnnos(func.args); for (args_anno_slice) |arg_anno_idx| { - try self.checkAnnotation(arg_anno_idx); + try self.generateAnnoTypeInPlace(arg_anno_idx, env, ctx); } - try self.checkAnnotation(func.ret); + const args_var_slice: []Var = @ptrCast(args_anno_slice); + + try self.generateAnnoTypeInPlace(func.ret, env, ctx); + + const fn_type = inner_blk: { + if (func.effectful) { + break :inner_blk try self.types.mkFuncEffectful(args_var_slice, ModuleEnv.varFrom(func.ret)); + } else { + break :inner_blk try self.types.mkFuncPure(args_var_slice, ModuleEnv.varFrom(func.ret)); + } + }; + try self.unifyWith(anno_var, fn_type, env); + }, + .tag_union => |tag_union| { + const scratch_tags_top = self.scratch_tags.top(); + defer self.scratch_tags.clearFrom(scratch_tags_top); + + const tag_anno_slices = self.cir.store.sliceTypeAnnos(tag_union.tags); + for (tag_anno_slices) |tag_anno_idx| { + // Get the tag anno + const tag_type_anno = self.cir.store.getTypeAnno(tag_anno_idx); + try self.setVarRank(ModuleEnv.varFrom(tag_anno_idx), env); + + // If the child of the tag union is not a tag, then set as error + // Canonicalization should have reported this error + if (tag_type_anno != .tag) { + try self.unifyWith(anno_var, .err, env); + return; + } + const tag = tag_type_anno.tag; + + // Generate the types for each tag arg + const tag_anno_args_slice = self.cir.store.sliceTypeAnnos(tag.args); + for (tag_anno_args_slice) |tag_arg_idx| { + try self.generateAnnoTypeInPlace(tag_arg_idx, env, ctx); + } + const tag_vars_slice: []Var = @ptrCast(tag_anno_args_slice); + + // Add the processed tag to scratch + try self.scratch_tags.append(try self.types.mkTag( + tag.name, + tag_vars_slice, + )); + } + + // Get the slice of tags + const tags_slice = self.scratch_tags.sliceFromStart(scratch_tags_top); + std.mem.sort(types_mod.Tag, tags_slice, self.cir.common.getIdentStore(), comptime types_mod.Tag.sortByNameAsc); + + // Process the ext if it exists. Absence means it's a closed union + const ext_var = inner_blk: { + if (tag_union.ext) |ext_anno_idx| { + try self.generateAnnoTypeInPlace(ext_anno_idx, env, ctx); + break :inner_blk ModuleEnv.varFrom(ext_anno_idx); + } else { + break :inner_blk try self.freshFromContent(.{ .structure = .empty_tag_union }, env, anno_region); + } + }; + + // Set the anno's type + try self.unifyWith(anno_var, try self.types.mkTagUnion(tags_slice, ext_var), env); + }, + .tag => { + // Tags should only exist as direct children of tag_unions in type annotations. + // If we encounter a standalone tag here, it's a compiler bug in canonicalization. + std.debug.assert(false); + try self.unifyWith(anno_var, .err, env); + }, + .record => |rec| { + const scratch_record_fields_top = self.scratch_record_fields.top(); + defer self.scratch_record_fields.clearFrom(scratch_record_fields_top); + + const recs_anno_slice = self.cir.store.sliceAnnoRecordFields(rec.fields); + + for (recs_anno_slice) |rec_anno_idx| { + const rec_field = self.cir.store.getAnnoRecordField(rec_anno_idx); + + try self.generateAnnoTypeInPlace(rec_field.ty, env, ctx); + const record_field_var = ModuleEnv.varFrom(rec_field.ty); + + // Add the processed tag to scratch + try self.scratch_record_fields.append(types_mod.RecordField{ + .name = rec_field.name, + .var_ = record_field_var, + }); + } + + // Get the slice of record_fields + const record_fields_slice = self.scratch_record_fields.sliceFromStart(scratch_record_fields_top); + std.mem.sort(types_mod.RecordField, record_fields_slice, self.cir.common.getIdentStore(), comptime types_mod.RecordField.sortByNameAsc); + const fields_type_range = try self.types.appendRecordFields(record_fields_slice); + + // Process the ext if it exists. Absence (null) means it's a closed record. + const ext_var = if (rec.ext) |ext_anno_idx| blk: { + try self.generateAnnoTypeInPlace(ext_anno_idx, env, ctx); + break :blk ModuleEnv.varFrom(ext_anno_idx); + } else blk: { + break :blk try self.freshFromContent(.{ .structure = .empty_record }, env, anno_region); + }; + + // Create the type for the anno in the store + try self.unifyWith( + anno_var, + .{ .structure = types_mod.FlatType{ .record = .{ + .fields = fields_type_range, + .ext = ext_var, + } } }, + env, + ); + }, + .tuple => |tuple| { + const elems_anno_slice = self.cir.store.sliceTypeAnnos(tuple.elems); + for (elems_anno_slice) |arg_anno_idx| { + try self.generateAnnoTypeInPlace(arg_anno_idx, env, ctx); + } + const elems_range = try self.types.appendVars(@ptrCast(elems_anno_slice)); + try self.unifyWith(anno_var, .{ .structure = .{ .tuple = .{ .elems = elems_range } } }, env); }, .parens => |parens| { - try self.checkAnnotation(parens.anno); + try self.generateAnnoTypeInPlace(parens.anno, env, ctx); + _ = try self.unify(anno_var, ModuleEnv.varFrom(parens.anno), env); + }, + .malformed => { + try self.unifyWith(anno_var, .err, env); }, - else => {}, } } -/// Check and process a type application annotation (e.g., Maybe(x), List(String)). +/// Set the content of anno_var to the builtin type. /// -/// This function handles annotations where a polymorphic type is applied with specific -/// type arguments. It creates a mapping from the type definition's rigid variables -/// to the annotation's rigid variables, preserving rigid constraints. -/// -/// Example: -/// Type definition: Maybe(a) := [Some(a), None] -/// Annotation: Maybe(x) -/// Result: Creates mapping a[rigid] → x[rigid], then applies it -/// to get [Some(x[rigid]), None] where x stays rigid -/// -/// Parameters: -/// * `anno_var` - Variable for the annotation (initially points to the type definition) -/// * `anno_region` - Source region for error reporting -/// * `anno_args_span` - The type arguments in the annotation (e.g., 'x' in Maybe(x)) -pub fn checkApplyAnno( +/// Uses unifyWith to efficiently set content directly when possible, +/// avoiding the creation of an intermediate type variable. +fn setBuiltinTypeContent( self: *Self, anno_var: Var, + anno_builtin_name: Ident.Idx, + anno_builtin_type: CIR.TypeAnno.Builtin, + anno_args: []Var, anno_region: Region, - anno_args_span: CIR.TypeAnno.Span, + env: *Env, ) std.mem.Allocator.Error!void { - // Clear any previous rigid variable mappings - self.annotation_rigid_var_subs.items.clearRetainingCapacity(); + switch (anno_builtin_type) { + // Phase 5: Use nominal types from Builtin instead of special .num content + .u8 => try self.unifyWith(anno_var, try self.mkNumberTypeContent("U8", env), env), + .u16 => try self.unifyWith(anno_var, try self.mkNumberTypeContent("U16", env), env), + .u32 => try self.unifyWith(anno_var, try self.mkNumberTypeContent("U32", env), env), + .u64 => try self.unifyWith(anno_var, try self.mkNumberTypeContent("U64", env), env), + .u128 => try self.unifyWith(anno_var, try self.mkNumberTypeContent("U128", env), env), + .i8 => try self.unifyWith(anno_var, try self.mkNumberTypeContent("I8", env), env), + .i16 => try self.unifyWith(anno_var, try self.mkNumberTypeContent("I16", env), env), + .i32 => try self.unifyWith(anno_var, try self.mkNumberTypeContent("I32", env), env), + .i64 => try self.unifyWith(anno_var, try self.mkNumberTypeContent("I64", env), env), + .i128 => try self.unifyWith(anno_var, try self.mkNumberTypeContent("I128", env), env), + .f32 => try self.unifyWith(anno_var, try self.mkNumberTypeContent("F32", env), env), + .f64 => try self.unifyWith(anno_var, try self.mkNumberTypeContent("F64", env), env), + .dec => try self.unifyWith(anno_var, try self.mkNumberTypeContent("Dec", env), env), + .list => { + // Then check arity + if (anno_args.len != 1) { + _ = try self.problems.appendProblem(self.gpa, .{ .type_apply_mismatch_arities = .{ + .type_name = anno_builtin_name, + .region = anno_region, + .num_expected_args = 1, + .num_actual_args = @intCast(anno_args.len), + } }); - // Get the base type definition that this annotation references - const type_base = self.types.resolveVar(anno_var).desc.content; - - const anno_args = self.cir.store.sliceTypeAnnos(anno_args_span); - - switch (type_base) { - .alias => |alias| { - const base_arg_vars = self.types.sliceAliasArgs(alias); - try self.buildRigidVarMapping(alias.ident.ident_idx, base_arg_vars, anno_args, anno_var); - }, - .structure => |flat_type| switch (flat_type) { - .nominal_type => |nominal| { - const base_arg_vars = self.types.sliceNominalArgs(nominal); - try self.buildRigidVarMapping(nominal.ident.ident_idx, base_arg_vars, anno_args, anno_var); - }, - else => { - // Do we need to handle cases like `List` or `Box` here? They - // have args, but it depends how they're added to scope in - // canonicalize. If they're added as aliases, then we don't, but - // if when we lookup `List` in scope, for example, we use the - // primitive directly, then we do - - }, - }, - else => { - // Non-parameterized types don't need rigid variable mapping - }, - } - - // Apply the rigid variable substitution to the type definition - // This converts the base type (e.g., Maybe(a)) to use the annotation's - // rigid variables (e.g., Maybe(x)) while preserving rigidity - const instantiated_var = try self.instantiateVar( - anno_var, - &self.annotation_rigid_var_subs, - .{ .explicit = anno_region }, - ); - - // Redirect the annotation variable to point to the substituted type - try self.types.setVarRedirect(anno_var, instantiated_var); -} - -/// Build the mapping from base type rigid variables to annotation rigid variables -/// -/// For each parameter in the base type definition, if it's a rigid variable, -/// map it to the corresponding argument in the annotation. -/// -/// Example: Maybe(a) + annotation args [x] → mapping["a"] = x[rigid] -fn buildRigidVarMapping( - self: *Self, - base_name: Ident.Idx, - base_arg_vars: []const types_mod.Var, - anno_args: []const CIR.TypeAnno.Idx, - anno_var: types_mod.Var, -) std.mem.Allocator.Error!void { - // Ensure we have the same number of parameters and arguments - if (base_arg_vars.len != anno_args.len) { - _ = try self.problems.appendProblem(self.gpa, .{ .type_apply_mismatch_arities = .{ - .type_name = base_name, - .anno_var = anno_var, - .num_expected_args = @intCast(base_arg_vars.len), - .num_actual_args = @intCast(anno_args.len), - } }); - // Base type parameter is in error state - propagate error - try self.types.setVarContent(anno_var, .err); - return; - } - - for (base_arg_vars, anno_args) |base_arg_var, anno_arg_idx| { - const base_arg_resolved = self.types.resolveVar(base_arg_var).desc.content; - - switch (base_arg_resolved) { - .rigid_var => |ident| { - // Found a rigid variable in the base type - map it to the annotation argument - const ident_text = self.cir.getIdent(ident); - - const anno_arg_var = ModuleEnv.varFrom(anno_arg_idx); - const anno_arg_var_inst = try self.instantiateVar(anno_arg_var, &self.annotation_rigid_var_subs, .use_last_var); - - try self.annotation_rigid_var_subs.append(self.gpa, .{ - .ident = ident_text, - .var_ = anno_arg_var_inst, - }); - }, - .err => { - // Base type parameter is in error state - propagate error - try self.types.setVarContent(anno_var, .err); + // Set error + try self.unifyWith(anno_var, .err, env); return; - }, - else => { - // Base type parameter is not a rigid variable (unexpected) - // This should only happen for rigid variables in well-formed type definitions - std.debug.assert(base_arg_resolved != .flex_var); - }, - } + } + + // Create the nominal List type + const list_content = try self.mkListContent(anno_args[0], env); + try self.unifyWith(anno_var, list_content, env); + }, + .box => { + // Then check arity + if (anno_args.len != 1) { + _ = try self.problems.appendProblem(self.gpa, .{ .type_apply_mismatch_arities = .{ + .type_name = anno_builtin_name, + .region = anno_region, + .num_expected_args = 1, + .num_actual_args = @intCast(anno_args.len), + } }); + + // Set error + try self.unifyWith(anno_var, .err, env); + return; + } + + // Create the nominal Box type + const box_content = try self.mkBoxContent(anno_args[0]); + try self.unifyWith(anno_var, box_content, env); + }, + // Polymorphic Num type is a module, not a type itself + .num => { + // Set error - Num is a module containing numeric types, not a type + try self.unifyWith(anno_var, .err, env); + }, } } +// types // + +const Expected = union(enum) { + no_expectation, + expected: struct { var_: Var, from_annotation: bool }, +}; + // pattern // -/// Check the types for the provided pattern -pub fn checkPattern(self: *Self, pattern_idx: CIR.Pattern.Idx) std.mem.Allocator.Error!void { +/// Check the types for the provided pattern, saving the type in-place +fn checkPattern( + self: *Self, + pattern_idx: CIR.Pattern.Idx, + env: *Env, + expected: Expected, +) std.mem.Allocator.Error!void { + _ = try self.checkPatternHelp(pattern_idx, env, expected, .in_place); +} + +/// Check the types for the provided pattern, either as fresh var or in-place +fn checkPatternHelp( + self: *Self, + pattern_idx: CIR.Pattern.Idx, + env: *Env, + expected: Expected, + comptime out_var: OutVar, +) std.mem.Allocator.Error!Var { const trace = tracy.trace(@src()); defer trace.end(); const pattern = self.cir.store.getPattern(pattern_idx); const pattern_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(pattern_idx)); + const pattern_var = switch (comptime out_var) { + .fresh => try self.fresh(env, pattern_region), + .in_place => blk: { + try self.setVarRank(ModuleEnv.varFrom(pattern_idx), env); + break :blk ModuleEnv.varFrom(pattern_idx); + }, + }; + switch (pattern) { - .nominal => |p| { - const real_nominal_var = ModuleEnv.varFrom(p.nominal_type_decl); - const pattern_backing_var = ModuleEnv.varFrom(p.backing_pattern); - try self.checkNominal( - ModuleEnv.varFrom(pattern_idx), - pattern_region, - pattern_backing_var, - p.backing_type, - real_nominal_var, - ); + .assign => |_| { + // In the case of an assigned variable, set it to be a flex var initially. + // This will be refined based on how it's used. + try self.unifyWith(pattern_var, .{ .flex = Flex.init() }, env); }, - .nominal_external => |p| { - const resolved_external = try self.resolveVarFromExternal(p.module_idx, p.target_node_idx) orelse { - // If we could not copy the type, set error and continue - try self.types.setVarContent(ModuleEnv.varFrom(pattern_idx), .err); - return; - }; - const pattern_backing_var = ModuleEnv.varFrom(p.backing_pattern); - try self.checkNominal( - ModuleEnv.varFrom(pattern_idx), - pattern_region, - pattern_backing_var, - p.backing_type, - resolved_external.local_var, - ); + .underscore => |_| { + // Underscore can be anything + try self.unifyWith(pattern_var, .{ .flex = Flex.init() }, env); }, - .int_literal => |_| { - // Integer literal patterns have their type constraints (bits_needed, sign_needed) - // created during canonicalization. The type variable for this pattern was already - // created with the appropriate num_unbound or int_unbound content. - // When this pattern is unified with the match scrutinee, the numeric constraints - // will be checked and produce NumberDoesNotFit or NegativeUnsignedInt errors - // if there's a mismatch. + // str // + .str_literal => { + const str_var = try self.freshStr(env, pattern_region); + _ = try self.unify(pattern_var, str_var, env); }, + // as // .as => |p| { - try self.checkPattern(p.pattern); + const var_ = try self.checkPatternHelp(p.pattern, env, expected, out_var); + _ = try self.unify(var_, pattern_var, env); }, - .applied_tag => |p| { - const args_slice = self.cir.store.slicePatterns(p.args); - for (args_slice) |pat_idx| { - try self.checkPattern(pat_idx); + // tuple // + .tuple => |tuple| { + const elem_vars_slice = blk: { + switch (comptime out_var) { + .fresh => { + const scratch_vars_top = self.scratch_vars.top(); + defer self.scratch_vars.clearFrom(scratch_vars_top); + + // Check tuple elements + const elems_slice = self.cir.store.slicePatterns(tuple.patterns); + for (elems_slice) |single_elem_ptrn_idx| { + const elem_var = try self.checkPatternHelp(single_elem_ptrn_idx, env, .no_expectation, out_var); + try self.scratch_vars.append(elem_var); + } + + // Add to types store + break :blk try self.types.appendVars(self.scratch_vars.sliceFromStart(scratch_vars_top)); + }, + .in_place => { + // Check tuple elements + const elems_slice = self.cir.store.slicePatterns(tuple.patterns); + for (elems_slice) |single_elem_ptrn_idx| { + _ = try self.checkPatternHelp(single_elem_ptrn_idx, env, .no_expectation, out_var); + } + + // Add to types store + // Cast the elems idxs to vars (this works because Anno Idx are 1-1 with type Vars) + break :blk try self.types.appendVars(@ptrCast(elems_slice)); + }, + } + }; + + // Set the type in the store + try self.unifyWith(pattern_var, .{ .structure = .{ + .tuple = .{ .elems = elem_vars_slice }, + } }, env); + }, + // list // + .list => |list| { + const elems = self.cir.store.slicePatterns(list.patterns); + if (elems.len == 0) { + // Create a nominal List with a fresh unbound element type + const elem_var = try self.fresh(env, pattern_region); + const list_content = try self.mkListContent(elem_var, env); + try self.unifyWith(pattern_var, list_content, env); + } else { + + // Here, we use the list's 1st element as the element var to + // constrain the rest of the list + + // Check the first elem + const elem_var = try self.checkPatternHelp(elems[0], env, .no_expectation, out_var); + + // Iterate over the remaining elements + var last_elem_ptrn_idx = elems[0]; + for (elems[1..], 1..) |elem_ptrn_idx, i| { + const cur_elem_var = try self.checkPatternHelp(elem_ptrn_idx, env, .no_expectation, out_var); + + // Unify each element's var with the list's elem var + const result = try self.unify(elem_var, cur_elem_var, env); + self.setDetailIfTypeMismatch(result, problem.TypeMismatchDetail{ .incompatible_list_elements = .{ + .last_elem_idx = ModuleEnv.nodeIdxFrom(last_elem_ptrn_idx), + .incompatible_elem_index = @intCast(i), + .list_length = @intCast(elems.len), + } }); + + // If we errored, check the rest of the elements without comparing + // to the elem_var to catch their individual errors + if (!result.isOk()) { + for (elems[i + 1 ..]) |remaining_elem_expr_idx| { + _ = try self.checkPatternHelp(remaining_elem_expr_idx, env, .no_expectation, out_var); + } + + // Break to avoid cascading errors + break; + } + + last_elem_ptrn_idx = elem_ptrn_idx; + } + + // Create a nominal List type with the inferred element type + const list_content = try self.mkListContent(elem_var, env); + try self.unifyWith(pattern_var, list_content, env); + + // Then, check the "rest" pattern is bound to a variable + // This is if the pattern is like `.. as x` + if (list.rest_info) |rest_info| { + if (rest_info.pattern) |rest_pattern_idx| { + const rest_pattern_var = try self.checkPatternHelp(rest_pattern_idx, env, .no_expectation, out_var); + + _ = try self.unify(pattern_var, rest_pattern_var, env); + } + } } }, - .tuple => |p| { - const args_slice = self.cir.store.slicePatterns(p.patterns); - for (args_slice) |pat_idx| { - try self.checkPattern(pat_idx); + // applied tag // + .applied_tag => |applied_tag| { + // Create a tag type in the type system and assign it the expr_var + + const arg_vars_slice = blk: { + switch (comptime out_var) { + .fresh => { + const scratch_vars_top = self.scratch_vars.top(); + defer self.scratch_vars.clearFrom(scratch_vars_top); + + // Check tuple elements + const arg_ptrn_idx_slice = self.cir.store.slicePatterns(applied_tag.args); + for (arg_ptrn_idx_slice) |arg_expr_idx| { + const arg_var = try self.checkPatternHelp(arg_expr_idx, env, .no_expectation, out_var); + try self.scratch_vars.append(arg_var); + } + + // Add to types store + break :blk try self.types.appendVars(self.scratch_vars.sliceFromStart(scratch_vars_top)); + }, + + .in_place => { + // Process each tag arg + const arg_ptrn_idx_slice = self.cir.store.slicePatterns(applied_tag.args); + for (arg_ptrn_idx_slice) |arg_expr_idx| { + _ = try self.checkPatternHelp(arg_expr_idx, env, .no_expectation, out_var); + } + + // Add to types store + // Cast the elems idxs to vars (this works because Anno Idx are 1-1 with type Vars) + break :blk try self.types.appendVars(@ptrCast(arg_ptrn_idx_slice)); + }, + } + }; + + // Create the type + const ext_var = try self.fresh(env, pattern_region); + + const tag = types_mod.Tag{ .name = applied_tag.name, .args = arg_vars_slice }; + const tag_union_content = try self.types.mkTagUnion(&[_]types_mod.Tag{tag}, ext_var); + + // Update the expr to point to the new type + try self.unifyWith(pattern_var, tag_union_content, env); + }, + // nominal // + .nominal => |nominal| { + // Check the backing pattern first + const actual_backing_var = try self.checkPatternHelp(nominal.backing_pattern, env, .no_expectation, out_var); + + // Use shared nominal type checking logic + _ = try self.checkNominalTypeUsage( + pattern_var, + actual_backing_var, + ModuleEnv.varFrom(nominal.nominal_type_decl), + nominal.backing_type, + pattern_region, + env, + ); + }, + .nominal_external => |nominal| { + // Check the backing pattern first + const actual_backing_var = try self.checkPatternHelp(nominal.backing_pattern, env, .no_expectation, out_var); + + // Resolve the external type declaration + if (try self.resolveVarFromExternal(nominal.module_idx, nominal.target_node_idx)) |ext_ref| { + // Use shared nominal type checking logic + _ = try self.checkNominalTypeUsage( + pattern_var, + actual_backing_var, + ext_ref.local_var, + nominal.backing_type, + pattern_region, + env, + ); + } else { + try self.unifyWith(pattern_var, .err, env); } }, - .record_destructure => |p| { - const destructs_slice = self.cir.store.sliceRecordDestructs(p.destructs); - for (destructs_slice) |destruct_idx| { + // record destructure // + .record_destructure => |destructure| { + const scratch_records_top = self.scratch_record_fields.top(); + defer self.scratch_record_fields.clearFrom(scratch_records_top); + + for (self.cir.store.sliceRecordDestructs(destructure.destructs)) |destruct_idx| { const destruct = self.cir.store.getRecordDestruct(destruct_idx); - try self.checkPattern(destruct.kind.toPatternIdx()); + const destruct_var = ModuleEnv.varFrom(destruct_idx); + try self.setVarRank(destruct_var, env); + + // Check the sub pattern + const field_pattern_var = blk: { + switch (destruct.kind) { + .Required => |sub_pattern_idx| { + break :blk try self.checkPatternHelp(sub_pattern_idx, env, .no_expectation, out_var); + }, + .SubPattern => |sub_pattern_idx| { + break :blk try self.checkPatternHelp(sub_pattern_idx, env, .no_expectation, out_var); + }, + } + }; + + // Set the destruct var to redirect to the field pattern var + _ = try self.unify(destruct_var, field_pattern_var, env); + + // Append it to the scratch records array + try self.scratch_record_fields.append(types_mod.RecordField{ + .name = destruct.label, + .var_ = ModuleEnv.varFrom(destruct_var), + }); + } + + // Copy the scratch record fields into the types store + const record_fields_scratch = self.scratch_record_fields.sliceFromStart(scratch_records_top); + std.mem.sort(types_mod.RecordField, record_fields_scratch, self.cir.getIdentStore(), comptime types_mod.RecordField.sortByNameAsc); + const record_fields_range = try self.types.appendRecordFields(record_fields_scratch); + + // Update the pattern var + try self.unifyWith(pattern_var, .{ .structure = .{ + .record_unbound = record_fields_range, + } }, env); + }, + // nums // + .num_literal => |num| { + // For unannotated literals (.num_unbound, .int_unbound), create a flex var with from_numeral constraint + switch (num.kind) { + .num_unbound, .int_unbound => { + // Create NumeralInfo for constraint checking + const num_literal_info = switch (num.value.kind) { + .u128 => types_mod.NumeralInfo.fromU128(@bitCast(num.value.bytes), false, pattern_region), + .i128 => types_mod.NumeralInfo.fromI128(num.value.toI128(), num.value.toI128() < 0, false, pattern_region), + }; + + // Create flex var with from_numeral constraint + const flex_var = try self.mkFlexWithFromNumeralConstraint(num_literal_info, env); + _ = try self.unify(pattern_var, flex_var, env); + }, + // Phase 5: For explicitly typed literals, use nominal types from Builtin + .u8 => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("U8", env), env), + .i8 => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("I8", env), env), + .u16 => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("U16", env), env), + .i16 => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("I16", env), env), + .u32 => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("U32", env), env), + .i32 => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("I32", env), env), + .u64 => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("U64", env), env), + .i64 => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("I64", env), env), + .u128 => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("U128", env), env), + .i128 => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("I128", env), env), + .f32 => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("F32", env), env), + .f64 => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("F64", env), env), + .dec => try self.unifyWith(pattern_var, try self.mkNumberTypeContent("Dec", env), env), } }, - else => {}, + .frac_f32_literal => |_| { + // Phase 5: Use nominal F32 type + try self.unifyWith(pattern_var, try self.mkNumberTypeContent("F32", env), env); + }, + .frac_f64_literal => |_| { + // Phase 5: Use nominal F64 type + try self.unifyWith(pattern_var, try self.mkNumberTypeContent("F64", env), env); + }, + .dec_literal => |dec| { + if (dec.has_suffix) { + // Explicit suffix like `3.14dec` - use nominal Dec type + try self.unifyWith(pattern_var, try self.mkNumberTypeContent("Dec", env), env); + } else { + // Unannotated decimal literal - create flex var with from_numeral constraint + const num_literal_info = types_mod.NumeralInfo.fromI128( + dec.value.num, // RocDec has .num field which is i128 scaled by 10^18 + dec.value.num < 0, + true, // Decimal literals are always fractional + pattern_region, + ); + + const flex_var = try self.mkFlexWithFromNumeralConstraint(num_literal_info, env); + _ = try self.unify(pattern_var, flex_var, env); + } + }, + .small_dec_literal => |dec| { + if (dec.has_suffix) { + // Explicit suffix - use nominal Dec type + try self.unifyWith(pattern_var, try self.mkNumberTypeContent("Dec", env), env); + } else { + // Unannotated decimal literal - create flex var with from_numeral constraint + // SmallDecValue stores a numerator (i16) and power of ten + // We need to convert this to an i128 scaled by 10^18 for consistency + const scaled_value = @as(i128, dec.value.numerator) * std.math.pow(i128, 10, 18 - dec.value.denominator_power_of_ten); + const num_literal_info = types_mod.NumeralInfo.fromI128( + scaled_value, + dec.value.numerator < 0, + true, + pattern_region, + ); + + const flex_var = try self.mkFlexWithFromNumeralConstraint(num_literal_info, env); + _ = try self.unify(pattern_var, flex_var, env); + } + }, + .runtime_error => { + try self.unifyWith(pattern_var, .err, env); + }, } + + // If we were provided with an expected type, unify against it + switch (expected) { + .no_expectation => {}, + .expected => |expected_type| { + if (expected_type.from_annotation) { + _ = try self.unifyWithCtx(expected_type.var_, pattern_var, env, .anno); + } else { + _ = try self.unify(expected_type.var_, pattern_var, env); + } + }, + } + + return pattern_var; } // expr // -/// Check the types for an exprexpression. Returns whether evaluating the expr might perform side effects. -pub fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx) std.mem.Allocator.Error!bool { - return self.checkExprWithExpected(expr_idx, null); -} - -/// Check expression with an optional expected type for bidirectional type checking -pub fn checkExprWithExpected(self: *Self, expr_idx: CIR.Expr.Idx, expected_type: ?Var) std.mem.Allocator.Error!bool { - return self.checkExprWithExpectedAndAnnotation(expr_idx, expected_type, false); -} - -fn checkExprWithExpectedAndAnnotation(self: *Self, expr_idx: CIR.Expr.Idx, expected_type: ?Var, from_annotation: bool) std.mem.Allocator.Error!bool { - const does_fx = self.checkExprWithExpectedAndAnnotationHelp(expr_idx, expected_type, from_annotation); - if (expected_type) |expected| { - if (from_annotation) { - _ = try self.unifyWithAnnotation(ModuleEnv.varFrom(expr_idx), expected); - } else { - _ = try self.unify(ModuleEnv.varFrom(expr_idx), expected); - } - } - return does_fx; -} - -/// Do not use directly, use `checkExprWithExpectedAndAnnotation` -/// -/// Checks the types of an expression, optionally against -fn checkExprWithExpectedAndAnnotationHelp(self: *Self, expr_idx: CIR.Expr.Idx, expected_type: ?Var, from_annotation: bool) std.mem.Allocator.Error!bool { +fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) std.mem.Allocator.Error!bool { const trace = tracy.trace(@src()); defer trace.end(); @@ -849,1206 +2759,1572 @@ fn checkExprWithExpectedAndAnnotationHelp(self: *Self, expr_idx: CIR.Expr.Idx, e const expr_var = ModuleEnv.varFrom(expr_idx); const expr_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(expr_idx)); + // Set the rank of the expr var, if it is not a lambda + // + // Lambdas push a new rank, so the var must be added to _that_ rank + if (expr != .e_lambda) { + try self.setVarRank(expr_var, env); + } + var does_fx = false; // Does this expression potentially perform any side effects? + switch (expr) { - .e_int => |_| { - // Integer literals have their type constraints (bits_needed, sign_needed) - // created during canonicalization. Here we just need to ensure those - // constraints will be checked when unified with expected types. - // The type variable for this expression was already created with the - // appropriate num_unbound or int_unbound content during canonicalization. - - // If we have an expected type, unify immediately to constrain the literal - if (expected_type) |expected| { - const literal_var = @as(Var, @enumFromInt(@intFromEnum(expr_idx))); - if (from_annotation) { - _ = try self.unifyWithAnnotation(literal_var, expected); - } else { - _ = try self.unify(literal_var, expected); - } - } + // str // + .e_str_segment => |_| { + const str_var = try self.freshStr(env, expr_region); + _ = try self.unify(expr_var, str_var, env); }, - .e_frac_f32 => |_| { - // Fractional literals have their type constraints (fits_in_f32, fits_in_dec) - // created during canonicalization. No additional checking needed here. - }, - .e_frac_f64 => |_| { - // Fractional literals have their type constraints (fits_in_f32, fits_in_dec) - // created during canonicalization. No additional checking needed here. - }, - .e_frac_dec => |_| { - // Decimal literals are similar to frac_f64. - }, - .e_dec_small => |_| { - // Small decimal literals are similar to frac_f64. - }, - .e_str_segment => |_| {}, - .e_str => |_| {}, - .e_lookup_local => |local| { - // For lookups, we need to connect the lookup expression to the actual variable - // The lookup expression should have the same type as the pattern it refers to - const lookup_var = @as(Var, @enumFromInt(@intFromEnum(expr_idx))); - const pattern_var = @as(Var, @enumFromInt(@intFromEnum(local.pattern_idx))); + .e_str => |str| { + // Iterate over the string segments, checking each one + const segment_expr_idx_slice = self.cir.store.sliceExpr(str.span); + var did_err = false; + for (segment_expr_idx_slice) |seg_expr_idx| { + const seg_expr = self.cir.store.getExpr(seg_expr_idx); - _ = try self.unify(lookup_var, pattern_var); - }, - .e_lookup_external => |e| { - const module_idx = @intFromEnum(e.module_idx); - if (module_idx < self.other_modules.len) { - const other_module_cir = self.other_modules[module_idx]; - const other_module_env = other_module_cir; - - // The idx of the expression in the other module - const target_node_idx = @as(CIR.Node.Idx, @enumFromInt(e.target_node_idx)); - - // Check if we've already copied this import - const cache_key = ImportCacheKey{ - .module_idx = e.module_idx, - .node_idx = target_node_idx, - }; - - const copied_var = if (self.import_cache.get(cache_key)) |cached_var| - // Reuse the previously copied type. - cached_var - else blk: { - // First time importing this type - copy it and cache the result - const imported_var = @as(Var, @enumFromInt(@intFromEnum(target_node_idx))); - const new_copy = try self.copyVar(imported_var, other_module_env); - try self.import_cache.put(self.gpa, cache_key, new_copy); - break :blk new_copy; - }; - const instantiated_copy = try self.instantiateVarAnon(copied_var, .use_last_var); - - // Unify our expression with the copied type - const result = try self.unify(expr_var, instantiated_copy); - if (result.isProblem()) { - self.setProblemTypeMismatchDetail(result.problem, .{ - .cross_module_import = .{ - .import_region = expr_idx, - .module_idx = e.module_idx, - }, - }); - - try self.types.setVarContent(expr_var, .err); - } - } else { - // Import not found - try self.types.setVarContent(expr_var, .err); - } - }, - .e_list => |list| { - const elem_var = @as(Var, @enumFromInt(@intFromEnum(list.elem_var))); - const elems = self.cir.store.exprSlice(list.elems); - - std.debug.assert(elems.len > 0); // Should never be 0 here, because this is not an .empty_list - - // We need to type-check the first element, but we don't need to unify it with - // anything because we already pre-unified the list's elem var with it. - const first_elem_idx = elems[0]; - var last_elem_idx: CIR.Expr.Idx = first_elem_idx; - does_fx = try self.checkExpr(first_elem_idx) or does_fx; - - for (elems[1..], 1..) |elem_expr_id, i| { - does_fx = try self.checkExpr(elem_expr_id) or does_fx; - - // Unify each element's var with the list's elem var - const result = try self.unify(elem_var, @enumFromInt(@intFromEnum(elem_expr_id))); - self.setDetailIfTypeMismatch(result, problem.TypeMismatchDetail{ .incompatible_list_elements = .{ - .last_elem_expr = last_elem_idx, - .incompatible_elem_index = @intCast(i), - .list_length = @intCast(elems.len), - } }); - - if (!result.isOk()) { - // Check remaining elements to catch their individual errors - for (elems[i + 1 ..]) |remaining_elem_id| { - does_fx = try self.checkExpr(remaining_elem_id) or does_fx; - } - - // Break to avoid cascading errors - break; - } - - last_elem_idx = elem_expr_id; - } - }, - .e_empty_list => |_| {}, - .e_match => |match| { - does_fx = try self.checkMatchExpr(expr_idx, match); - }, - .e_if => |if_expr| { - does_fx = try self.checkIfElseExpr(expr_idx, expr_region, if_expr); - }, - .e_call => |call| { - // Get all expressions - first is function, rest are arguments - const all_exprs = self.cir.store.sliceExpr(call.args); - - if (all_exprs.len == 0) return false; // No function to call - - // First expression is the function being called; the rest are args. - const func_expr_idx = all_exprs[0]; - does_fx = try self.checkExpr(func_expr_idx) or does_fx; // func_expr could be effectful, e.g. `(mk_fn!())(arg)` - - // Then, check all the arguments - const call_args = all_exprs[1..]; - for (call_args) |arg_expr_idx| { - // Each arg could also be effectful, e.g. `fn(mk_arg!(), mk_arg!())` - does_fx = try self.checkExpr(arg_expr_idx) or does_fx; - } - - // Don't try to unify with the function if the function is a runtime error. - const func_expr = self.cir.store.getExpr(func_expr_idx); - if (func_expr != .e_runtime_error) { - const func_expr_region = self.cir.store.getRegionAt(ModuleEnv.nodeIdxFrom(func_expr_idx)); - - const call_var = @as(Var, @enumFromInt(@intFromEnum(expr_idx))); - const call_func_var = @as(Var, @enumFromInt(@intFromEnum(func_expr_idx))); - const resolved_func = self.types.resolveVar(call_func_var); - - // Check if this is an annotated function that needs instantiation - // We only instantiate if the function actually contains type variables - var cur_call_func_var = call_func_var; - var current_content = resolved_func.desc.content; - - content_switch: switch (current_content) { - .structure => |flat_type| switch (flat_type) { - .fn_effectful => |_| { - does_fx = true; - if (self.types.needsInstantiation(cur_call_func_var)) { - const expected_func_var = try self.instantiateVarAnon(cur_call_func_var, .{ .explicit = expr_region }); - const resolved_expected_func = self.types.resolveVar(expected_func_var); - - std.debug.assert(resolved_expected_func.desc.content == .structure); - std.debug.assert(resolved_expected_func.desc.content.structure == .fn_effectful); - const expected_func = resolved_expected_func.desc.content.structure.fn_effectful; - - does_fx = try self.unifyFunctionCall(call_var, call_func_var, call_args, expected_func, expr_region, func_expr_idx) or does_fx; - - // Unify with expected type if provided - if (expected_type) |expected| { - if (from_annotation) { - _ = try self.unifyWithAnnotation(call_var, expected); - } else { - _ = try self.unify(call_var, expected); - } - } - - return does_fx; - } - }, - .fn_pure => |_| { - if (self.types.needsInstantiation(cur_call_func_var)) { - const expected_func_var = try self.instantiateVarAnon(cur_call_func_var, .{ .explicit = expr_region }); - const resolved_expected_func = self.types.resolveVar(expected_func_var); - - std.debug.assert(resolved_expected_func.desc.content == .structure); - std.debug.assert(resolved_expected_func.desc.content.structure == .fn_pure); - const expected_func = resolved_expected_func.desc.content.structure.fn_pure; - - does_fx = try self.unifyFunctionCall(call_var, call_func_var, call_args, expected_func, func_expr_region, func_expr_idx) or does_fx; - - // Unify with expected type if provided - if (expected_type) |expected| { - if (from_annotation) { - _ = try self.unifyWithAnnotation(call_var, expected); - } else { - _ = try self.unify(call_var, expected); - } - } - - return does_fx; - } - }, - .fn_unbound => |_| { - if (self.types.needsInstantiation(cur_call_func_var)) { - const expected_func_var = try self.instantiateVarAnon(cur_call_func_var, .{ .explicit = expr_region }); - const resolved_expected_func = self.types.resolveVar(expected_func_var); - - std.debug.assert(resolved_expected_func.desc.content == .structure); - std.debug.assert(resolved_expected_func.desc.content.structure == .fn_unbound); - const expected_func = resolved_expected_func.desc.content.structure.fn_unbound; - - does_fx = try self.unifyFunctionCall(call_var, call_func_var, call_args, expected_func, expr_region, func_expr_idx) or does_fx; - return does_fx; - } - }, - else => { - // Non-function structure - fall through - }, - }, - .alias => |alias| { - // Resolve the alias, then continue on to the appropriate branch. - // (It might be another alias, or we might be done and ready to proceed.) - const backing_var = self.types.getAliasBackingVar(alias); - cur_call_func_var = backing_var; - current_content = self.types.resolveVar(backing_var).desc.content; - continue :content_switch current_content; + // String literal segments are already Str type + switch (seg_expr) { + .e_str_segment => { + does_fx = try self.checkExpr(seg_expr_idx, env, .no_expectation) or does_fx; }, else => { - // Non-structure content - fall through + // Interpolated expressions must be of type Str + const seg_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(seg_expr_idx)); + const expected_str_var = try self.freshStr(env, seg_region); + does_fx = try self.checkExpr(seg_expr_idx, env, .{ .expected = .{ .var_ = expected_str_var, .from_annotation = false } }) or does_fx; + + // Unify the segment's type with Str to produce a type error if it doesn't match + const seg_var = ModuleEnv.varFrom(seg_expr_idx); + const unify_result = try self.unify(seg_var, expected_str_var, env); + if (!unify_result.isOk()) { + // Unification failed - mark as error + try self.unifyWith(seg_var, .err, env); + did_err = true; + } }, } - // We didn't handle the function call above (either because it wasn't a function - // or it didn't need instantiation), so fall back on this logic. - const arg_vars: []Var = @constCast(@ptrCast(@alignCast(call_args))); + // Check if it errored (for non-interpolation segments) + if (!did_err) { + const seg_var = ModuleEnv.varFrom(seg_expr_idx); + did_err = self.types.resolveVar(seg_var).desc.content == .err; + } + } - // Create an unbound function type with the call result as return type - // The unification will propagate the actual return type to the call - // - // TODO: Do we need to insert a CIR placeholder node here as well? - // What happens if later this type variable has a problem, and we - // try to look up its region in CIR? - const func_content = try self.types.mkFuncUnbound(arg_vars, call_var); - const expected_func_var = try self.freshFromContent(func_content, expr_region); - _ = try self.unify(expected_func_var, cur_call_func_var); + if (did_err) { + // If any segment errored, propgate that error to the root string + try self.unifyWith(expr_var, .err, env); + } else { + // Otherwise, set the type of this expr to be nominal Str + const str_var = try self.freshStr(env, expr_region); + _ = try self.unify(expr_var, str_var, env); } }, - .e_record => |e| { - // Perform field-by-field unification between the record structure's - // field type variables and the actual field value expression types. - // - // 1. Resolve the expression var to get the record structure - // 2. Type check each field value expression (to get concrete types) - // 3. For each field, unify the field type var with the field value type var - // 4. Unification propagates concrete types through the type system + // nums // + .e_num => |num| { + switch (num.kind) { + .num_unbound, .int_unbound => { + // For unannotated literals, create a flex var with from_numeral constraint + const num_literal_info = switch (num.value.kind) { + .u128 => types_mod.NumeralInfo.fromU128(@bitCast(num.value.bytes), false, expr_region), + .i128 => types_mod.NumeralInfo.fromI128(num.value.toI128(), num.value.toI128() < 0, false, expr_region), + }; - const record_var_resolved = self.types.resolveVar(expr_var); - const record_var_content = record_var_resolved.desc.content; + // Create flex var with from_numeral constraint + const flex_var = try self.mkFlexWithFromNumeralConstraint(num_literal_info, env); + _ = try self.unify(expr_var, flex_var, env); + + const resolved = self.types.resolveVar(flex_var); + const constraint_range = resolved.desc.content.flex.constraints; + const constraint = self.types.sliceStaticDispatchConstraints(constraint_range)[0]; + + // Record this literal for deferred validation during comptime eval + _ = try self.cir.deferred_numeric_literals.append(self.gpa, .{ + .expr_idx = expr_idx, + .type_var = flex_var, + .constraint = constraint, + .region = expr_region, + }); + }, + .u8 => try self.unifyWith(expr_var, try self.mkNumberTypeContent("U8", env), env), + .i8 => try self.unifyWith(expr_var, try self.mkNumberTypeContent("I8", env), env), + .u16 => try self.unifyWith(expr_var, try self.mkNumberTypeContent("U16", env), env), + .i16 => try self.unifyWith(expr_var, try self.mkNumberTypeContent("I16", env), env), + .u32 => try self.unifyWith(expr_var, try self.mkNumberTypeContent("U32", env), env), + .i32 => try self.unifyWith(expr_var, try self.mkNumberTypeContent("I32", env), env), + .u64 => try self.unifyWith(expr_var, try self.mkNumberTypeContent("U64", env), env), + .i64 => try self.unifyWith(expr_var, try self.mkNumberTypeContent("I64", env), env), + .u128 => try self.unifyWith(expr_var, try self.mkNumberTypeContent("U128", env), env), + .i128 => try self.unifyWith(expr_var, try self.mkNumberTypeContent("I128", env), env), + .f32 => try self.unifyWith(expr_var, try self.mkNumberTypeContent("F32", env), env), + .f64 => try self.unifyWith(expr_var, try self.mkNumberTypeContent("F64", env), env), + .dec => try self.unifyWith(expr_var, try self.mkNumberTypeContent("Dec", env), env), + } + }, + .e_frac_f32 => |frac| { + if (frac.has_suffix) { + try self.unifyWith(expr_var, try self.mkNumberTypeContent("F32", env), env); + } else { + // Unsuffixed fractional literal - create constrained flex var + const num_literal_info = types_mod.NumeralInfo.fromI128( + @as(i128, @as(u32, @bitCast(frac.value))), + frac.value < 0, + true, + expr_region, + ); + const flex_var = try self.mkFlexWithFromNumeralConstraint(num_literal_info, env); + _ = try self.unify(expr_var, flex_var, env); + + const resolved = self.types.resolveVar(flex_var); + const constraint_range = resolved.desc.content.flex.constraints; + const constraint = self.types.sliceStaticDispatchConstraints(constraint_range)[0]; + + _ = try self.cir.deferred_numeric_literals.append(self.gpa, .{ + .expr_idx = expr_idx, + .type_var = flex_var, + .constraint = constraint, + .region = expr_region, + }); + } + }, + .e_frac_f64 => |frac| { + if (frac.has_suffix) { + try self.unifyWith(expr_var, try self.mkNumberTypeContent("F64", env), env); + } else { + // Unsuffixed fractional literal - create constrained flex var + const num_literal_info = types_mod.NumeralInfo.fromI128( + @as(i128, @as(u64, @bitCast(frac.value))), + frac.value < 0, + true, + expr_region, + ); + const flex_var = try self.mkFlexWithFromNumeralConstraint(num_literal_info, env); + _ = try self.unify(expr_var, flex_var, env); + + const resolved = self.types.resolveVar(flex_var); + const constraint_range = resolved.desc.content.flex.constraints; + const constraint = self.types.sliceStaticDispatchConstraints(constraint_range)[0]; + + _ = try self.cir.deferred_numeric_literals.append(self.gpa, .{ + .expr_idx = expr_idx, + .type_var = flex_var, + .constraint = constraint, + .region = expr_region, + }); + } + }, + .e_dec => |frac| { + if (frac.has_suffix) { + try self.unifyWith(expr_var, try self.mkNumberTypeContent("Dec", env), env); + } else { + // Unsuffixed Dec literal - create constrained flex var + const num_literal_info = types_mod.NumeralInfo.fromI128( + frac.value.num, + frac.value.num < 0, + true, + expr_region, + ); + const flex_var = try self.mkFlexWithFromNumeralConstraint(num_literal_info, env); + _ = try self.unify(expr_var, flex_var, env); + + const resolved = self.types.resolveVar(flex_var); + const constraint_range = resolved.desc.content.flex.constraints; + const constraint = self.types.sliceStaticDispatchConstraints(constraint_range)[0]; + + _ = try self.cir.deferred_numeric_literals.append(self.gpa, .{ + .expr_idx = expr_idx, + .type_var = flex_var, + .constraint = constraint, + .region = expr_region, + }); + } + }, + .e_dec_small => |frac| { + if (frac.has_suffix) { + try self.unifyWith(expr_var, try self.mkNumberTypeContent("Dec", env), env); + } else { + // Unsuffixed small Dec literal - create constrained flex var + // Scale the value to i128 representation + const scaled_value = @as(i128, frac.value.numerator) * std.math.pow(i128, 10, 18 - frac.value.denominator_power_of_ten); + const num_literal_info = types_mod.NumeralInfo.fromI128( + scaled_value, + scaled_value < 0, + true, + expr_region, + ); + const flex_var = try self.mkFlexWithFromNumeralConstraint(num_literal_info, env); + _ = try self.unify(expr_var, flex_var, env); + + const resolved = self.types.resolveVar(flex_var); + const constraint_range = resolved.desc.content.flex.constraints; + const constraint = self.types.sliceStaticDispatchConstraints(constraint_range)[0]; + + _ = try self.cir.deferred_numeric_literals.append(self.gpa, .{ + .expr_idx = expr_idx, + .type_var = flex_var, + .constraint = constraint, + .region = expr_region, + }); + } + }, + // list // + .e_empty_list => { + // Create a nominal List with a fresh unbound element type + const elem_var = try self.fresh(env, expr_region); + const list_content = try self.mkListContent(elem_var, env); + try self.unifyWith(expr_var, list_content, env); + }, + .e_list => |list| { + const elems = self.cir.store.exprSlice(list.elems); + + if (elems.len == 0) { + // Create a nominal List with a fresh unbound element type + const elem_var = try self.fresh(env, expr_region); + const list_content = try self.mkListContent(elem_var, env); + try self.unifyWith(expr_var, list_content, env); + } else { + // Here, we use the list's 1st element as the element var to + // constrain the rest of the list + + // Check the first elem + does_fx = try self.checkExpr(elems[0], env, .no_expectation) or does_fx; + + // Iterate over the remaining elements + const elem_var = ModuleEnv.varFrom(elems[0]); + var last_elem_expr_idx = elems[0]; + for (elems[1..], 1..) |elem_expr_idx, i| { + does_fx = try self.checkExpr(elem_expr_idx, env, .no_expectation) or does_fx; + const cur_elem_var = ModuleEnv.varFrom(elem_expr_idx); + + // Unify each element's var with the list's elem var + const result = try self.unify(elem_var, cur_elem_var, env); + self.setDetailIfTypeMismatch(result, problem.TypeMismatchDetail{ .incompatible_list_elements = .{ + .last_elem_idx = ModuleEnv.nodeIdxFrom(last_elem_expr_idx), + .incompatible_elem_index = @intCast(i), + .list_length = @intCast(elems.len), + } }); + + // If we errored, check the rest of the elements without comparing + // to the elem_var to catch their individual errors + if (!result.isOk()) { + for (elems[i + 1 ..]) |remaining_elem_expr_idx| { + does_fx = try self.checkExpr(remaining_elem_expr_idx, env, .no_expectation) or does_fx; + } + + // Break to avoid cascading errors + break; + } + + last_elem_expr_idx = elem_expr_idx; + } + + // Create a nominal List type with the inferred element type + const list_content = try self.mkListContent(elem_var, env); + try self.unifyWith(expr_var, list_content, env); + } + }, + // tuple // + .e_tuple => |tuple| { + // Check tuple elements + const elems_slice = self.cir.store.exprSlice(tuple.elems); + for (elems_slice) |single_elem_expr_idx| { + does_fx = try self.checkExpr(single_elem_expr_idx, env, .no_expectation) or does_fx; + } + + // Cast the elems idxs to vars (this works because Anno Idx are 1-1 with type Vars) + const elem_vars_slice = try self.types.appendVars(@ptrCast(elems_slice)); + + // Set the type in the store + try self.unifyWith(expr_var, .{ .structure = .{ + .tuple = .{ .elems = elem_vars_slice }, + } }, env); + }, + // record // + .e_record => |e| { + // Create a record type in the type system and assign it the expr_var + + // Write down the top of the scratch records array + const record_fields_top = self.scratch_record_fields.top(); + defer self.scratch_record_fields.clearFrom(record_fields_top); // Process each field for (self.cir.store.sliceRecordFields(e.fields)) |field_idx| { const field = self.cir.store.getRecordField(field_idx); - // STEP 1: Check the field value expression first - // This ensures the field value has a concrete type to unify with - does_fx = try self.checkExpr(field.value) or does_fx; + // Check the field value expression + does_fx = try self.checkExpr(field.value, env, .no_expectation) or does_fx; - // STEP 2: Find the corresponding field type in the record structure - // This only works if record_var_content is .structure.record - if (record_var_content == .structure and record_var_content.structure == .record) { - const record_fields = self.types.getRecordFieldsSlice(record_var_content.structure.record.fields); + // Append it to the scratch records array + try self.scratch_record_fields.append(types_mod.RecordField{ + .name = field.name, + .var_ = ModuleEnv.varFrom(field.value), + }); + } - // STEP 3: Find the field with matching name and unify types - const field_names = record_fields.items(.name); - const field_vars = record_fields.items(.var_); - for (field_names, field_vars) |type_field_name, type_field_var| { - if (type_field_name.idx == field.name.idx) { - // Extract the type variable from the field value expression - // Different expression types store their type variables in different places - const field_expr_type_var = @as(Var, @enumFromInt(@intFromEnum(field.value))); + // Copy the scratch fields into the types store + const record_fields_scratch = self.scratch_record_fields.sliceFromStart(record_fields_top); + std.mem.sort(types_mod.RecordField, record_fields_scratch, self.cir.getIdentStore(), comptime types_mod.RecordField.sortByNameAsc); + const record_fields_range = try self.types.appendRecordFields(record_fields_scratch); - // STEP 4: Unify field type variable with field value type variable - // This is where concrete types (like Str, Num) get propagated - // from field values to the record structure - _ = try self.unify(type_field_var, field_expr_type_var); - break; + // Check if this is a record update + if (e.ext) |record_being_updated_expr| { + // Create an unbound record with the provided fields + const ext_var = try self.fresh(env, expr_region); + try self.unifyWith(expr_var, .{ .structure = .{ + .record = .{ + .fields = record_fields_range, + .ext = ext_var, + }, + } }, env); + + does_fx = try self.checkExpr(record_being_updated_expr, env, .no_expectation) or does_fx; + const record_being_updated_var = ModuleEnv.varFrom(record_being_updated_expr); + + _ = try self.unify(record_being_updated_var, expr_var, env); + } else { + // Create an unbound record with the provided fields + const ext_var = try self.freshFromContent(.{ .structure = .empty_record }, env, expr_region); + try self.unifyWith(expr_var, .{ .structure = .{ .record = .{ + .fields = record_fields_range, + .ext = ext_var, + } } }, env); + } + }, + .e_empty_record => { + try self.unifyWith(expr_var, .{ .structure = .empty_record }, env); + }, + // tags // + .e_zero_argument_tag => |e| { + const ext_var = try self.fresh(env, expr_region); + + const tag = try self.types.mkTag(e.name, &.{}); + const tag_union_content = try self.types.mkTagUnion(&[_]types_mod.Tag{tag}, ext_var); + + // Update the expr to point to the new type + try self.unifyWith(expr_var, tag_union_content, env); + }, + .e_tag => |e| { + // Create a tag type in the type system and assign it the expr_var + + // Process each tag arg + const arg_expr_idx_slice = self.cir.store.sliceExpr(e.args); + for (arg_expr_idx_slice) |arg_expr_idx| { + does_fx = try self.checkExpr(arg_expr_idx, env, .no_expectation) or does_fx; + } + + // Create the type + const ext_var = try self.fresh(env, expr_region); + + const tag = try self.types.mkTag(e.name, @ptrCast(arg_expr_idx_slice)); + const tag_union_content = try self.types.mkTagUnion(&[_]types_mod.Tag{tag}, ext_var); + + // Update the expr to point to the new type + try self.unifyWith(expr_var, tag_union_content, env); + }, + // nominal // + .e_nominal => |nominal| { + // Check the backing expression first + does_fx = try self.checkExpr(nominal.backing_expr, env, .no_expectation) or does_fx; + const actual_backing_var = ModuleEnv.varFrom(nominal.backing_expr); + + // Use shared nominal type checking logic + _ = try self.checkNominalTypeUsage( + expr_var, + actual_backing_var, + ModuleEnv.varFrom(nominal.nominal_type_decl), + nominal.backing_type, + expr_region, + env, + ); + }, + .e_nominal_external => |nominal| { + // Check the backing expression first + does_fx = try self.checkExpr(nominal.backing_expr, env, .no_expectation) or does_fx; + const actual_backing_var = ModuleEnv.varFrom(nominal.backing_expr); + + // Resolve the external type declaration + if (try self.resolveVarFromExternal(nominal.module_idx, nominal.target_node_idx)) |ext_ref| { + // Use shared nominal type checking logic + _ = try self.checkNominalTypeUsage( + expr_var, + actual_backing_var, + ext_ref.local_var, + nominal.backing_type, + expr_region, + env, + ); + } else { + try self.unifyWith(expr_var, .err, env); + } + }, + // lookup // + .e_lookup_local => |lookup| { + const mb_processing_def = self.top_level_ptrns.get(lookup.pattern_idx); + if (mb_processing_def) |processing_def| { + switch (processing_def.status) { + .not_processed => { + var sub_env = try self.env_pool.acquire(.generalized); + defer self.env_pool.release(sub_env); + + try self.checkDef(processing_def.def_idx, &sub_env); + }, + .processing => { + // Recursive reference - the pattern variable is still at + // top_level rank (not generalized), so the code below will + // unify directly with it, which is the correct behavior. + }, + .processed => {}, + } + } + + const pat_var = ModuleEnv.varFrom(lookup.pattern_idx); + const resolved_pat = self.types.resolveVar(pat_var); + + // Check if this is a generalized var that should NOT be instantiated. + // Numeric literals with from_numeral constraints should unify directly + // so that the concrete type propagates back to the definition site. + // This fixes GitHub issue #8666 where polymorphic numerics defaulted to Dec. + const should_instantiate = blk: { + if (resolved_pat.desc.rank != Rank.generalized) break :blk false; + // Don't instantiate if this has a from_numeral constraint + if (resolved_pat.desc.content == .flex) { + const flex = resolved_pat.desc.content.flex; + const constraints = self.types.sliceStaticDispatchConstraints(flex.constraints); + for (constraints) |constraint| { + if (constraint.origin == .from_numeral) { + break :blk false; } } } - // If record_var_content is NOT .structure.record, unification is skipped - // This typically happens when canonicalization didn't set the record structure properly + break :blk true; + }; + + if (should_instantiate) { + const instantiated = try self.instantiateVar(pat_var, env, .use_last_var); + _ = try self.unify(expr_var, instantiated, env); + } else { + _ = try self.unify(expr_var, pat_var, env); + } + + // Unify this expression with the referenced pattern + }, + .e_lookup_external => |ext| { + if (try self.resolveVarFromExternal(ext.module_idx, ext.target_node_idx)) |ext_ref| { + const ext_instantiated_var = try self.instantiateVar( + ext_ref.local_var, + env, + .{ .explicit = expr_region }, + ); + _ = try self.unify(expr_var, ext_instantiated_var, env); + } else { + try self.unifyWith(expr_var, .err, env); } }, - .e_empty_record => |_| {}, - .e_tag => |_| {}, - .e_nominal => |e| { - const real_nominal_var = ModuleEnv.varFrom(e.nominal_type_decl); - const expr_backing_var = ModuleEnv.varFrom(e.backing_expr); - - try self.checkNominal( - ModuleEnv.varFrom(expr_idx), - expr_region, - expr_backing_var, - e.backing_type, - real_nominal_var, - ); - }, - .e_nominal_external => |e| { - const resolved_external = try self.resolveVarFromExternal(e.module_idx, e.target_node_idx) orelse { - // If we could not copy the type, set error and continue - try self.types.setVarContent(ModuleEnv.varFrom(expr_idx), .err); - return false; - }; - const expr_backing_var = ModuleEnv.varFrom(e.backing_expr); - try self.checkNominal( - ModuleEnv.varFrom(expr_idx), - expr_region, - expr_backing_var, - e.backing_type, - resolved_external.local_var, - ); - }, - .e_zero_argument_tag => |_| {}, - .e_binop => |binop| { - does_fx = try self.checkBinopExpr(expr_idx, expr_region, binop, expected_type, from_annotation); - }, - .e_unary_minus => |unary| { - does_fx = try self.checkUnaryMinusExpr(expr_idx, expr_region, unary); - }, - .e_unary_not => |unary| { - does_fx = try self.checkUnaryNotExpr(expr_idx, expr_region, unary); + .e_lookup_required => |req| { + // Look up the type from the platform's requires clause + const requires_items = self.cir.requires_types.items.items; + const idx = req.requires_idx.toU32(); + if (idx < requires_items.len) { + const required_type = requires_items[idx]; + const type_var = ModuleEnv.varFrom(required_type.type_anno); + const instantiated_var = try self.instantiateVar( + type_var, + env, + .{ .explicit = expr_region }, + ); + _ = try self.unify(expr_var, instantiated_var, env); + } else { + try self.unifyWith(expr_var, .err, env); + } }, + // block // .e_block => |block| { + const anno_free_vars_top = self.anno_free_vars.top(); + defer self.anno_free_vars.clearFrom(anno_free_vars_top); + // Check all statements in the block const statements = self.cir.store.sliceStatements(block.stmts); - for (statements) |stmt_idx| { - const stmt = self.cir.store.getStatement(stmt_idx); - switch (stmt) { - .s_decl => |decl_stmt| { - // Check pattern and expression, then unify - try self.checkPattern(decl_stmt.pattern); - if (decl_stmt.anno) |anno_idx| { - // TODO: When instantiating, use the parent type variables - // map here to allow `forall` behavior - const annotation = self.cir.store.getAnnotation(anno_idx); - try self.checkAnnotation(annotation.type_anno); - - does_fx = try self.checkExprWithExpectedAndAnnotation(decl_stmt.expr, ModuleEnv.varFrom(annotation.type_anno), true) or does_fx; - } else { - does_fx = try self.checkExpr(decl_stmt.expr) or does_fx; - } - - // Unify the pattern with the expression - const decl_pattern_var: Var = @enumFromInt(@intFromEnum(decl_stmt.pattern)); - const decl_expr_var: Var = @enumFromInt(@intFromEnum(decl_stmt.expr)); - _ = try self.unify(decl_pattern_var, decl_expr_var); - }, - .s_reassign => |reassign| { - does_fx = try self.checkExpr(reassign.expr) or does_fx; - }, - .s_expr => |expr_stmt| { - does_fx = try self.checkExpr(expr_stmt.expr) or does_fx; - }, - else => { - // Other statement types don't need expression checking - }, - } - } + const stmt_result = try self.checkBlockStatements(statements, env, expr_region); + does_fx = stmt_result.does_fx or does_fx; // Check the final expression - does_fx = try self.checkExpr(block.final_expr) or does_fx; + does_fx = try self.checkExpr(block.final_expr, env, expected) or does_fx; - // Link the root expr with the final expr - _ = try self.unify( - @enumFromInt(@intFromEnum(expr_idx)), - @enumFromInt(@intFromEnum(block.final_expr)), - ); - }, - .e_closure => |closure| { - // The type of a closure is the type of the lambda it wraps. - // The lambda's type is determined by its arguments and body. - // We need to check the lambda expression itself to get its type. - // If we have an expected type, pass it to the lambda to catch type errors early - if (expected_type) |expected| { - does_fx = try self.checkExprWithExpectedAndAnnotation(closure.lambda_idx, expected, from_annotation); + // If the block diverges (has a return/crash), use a flex var for the block's type + // since the final expression is unreachable + if (stmt_result.diverges) { + try self.unifyWith(expr_var, .{ .flex = Flex.init() }, env); } else { - does_fx = try self.checkExpr(closure.lambda_idx); + // Link the root expr with the final expr + _ = try self.unify(expr_var, ModuleEnv.varFrom(block.final_expr), env); } - const lambda_var = ModuleEnv.varFrom(closure.lambda_idx); - const closure_var = ModuleEnv.varFrom(expr_idx); - _ = try self.unify(closure_var, lambda_var); }, + // function // .e_lambda => |lambda| { - does_fx = try self.checkLambdaWithAnno(expr_idx, expr_region, lambda, expected_type); - }, - .e_tuple => |tuple| { - // Check tuple elements - const elems_slice = self.cir.store.exprSlice(tuple.elems); - for (elems_slice) |single_elem_expr_idx| { - does_fx = try self.checkExpr(single_elem_expr_idx) or does_fx; - } - - // The tuple type is created in the type store in canonicalize, so - // nothing more needs to be done here - }, - .e_dot_access => |dot_access| { - // Check the receiver expression - does_fx = try self.checkExpr(dot_access.receiver) or does_fx; - - // Get the type of the receiver - const receiver_var = @as(Var, @enumFromInt(@intFromEnum(dot_access.receiver))); - const resolved_receiver = self.types.resolveVar(receiver_var); - - // Handle different receiver types - switch (resolved_receiver.desc.content) { - .structure => |structure| switch (structure) { - .nominal_type => |nominal| { - // This is a static dispatch on a nominal type - if (dot_access.args) |args_span| { - // Method call with arguments - // Get the origin module path - const origin_module_path = self.cir.getIdent(nominal.origin_module); - - // Find which imported module matches this path - var origin_module_idx: ?CIR.Import.Idx = null; - var origin_module: ?*const ModuleEnv = null; - - // Check if it's the current module - if (std.mem.eql(u8, origin_module_path, self.cir.module_name)) { - origin_module = self.cir; - } else { - // Search through imported modules - for (self.other_modules, 0..) |other_module, idx| { - if (std.mem.eql(u8, origin_module_path, other_module.module_name)) { - origin_module_idx = @enumFromInt(idx); - origin_module = other_module; - break; - } - } - } - - if (origin_module) |module| { - // Look up the method in the origin module's exports - // Need to convert identifier from current module to target module - const method_name_str = self.cir.getIdent(dot_access.field_name); - - // Search through the module's exposed items - const node_idx_opt = if (module.common.findIdent(method_name_str)) |target_ident| - module.getExposedNodeIndexById(target_ident) - else - null; - - if (node_idx_opt) |node_idx| { - // Found the method! - const target_node_idx = @as(CIR.Node.Idx, @enumFromInt(node_idx)); - - // Check if we've already copied this import - const cache_key = ImportCacheKey{ - .module_idx = origin_module_idx orelse @enumFromInt(0), // Current module - .node_idx = target_node_idx, - }; - - const method_var = if (self.import_cache.get(cache_key)) |cached_var| - cached_var - else blk: { - // Copy the method's type from the origin module to our type store - const source_var = @as(Var, @enumFromInt(@intFromEnum(target_node_idx))); - const new_copy = try self.copyVar(source_var, @constCast(module)); - try self.import_cache.put(self.gpa, cache_key, new_copy); - break :blk new_copy; - }; - const method_instantiated = try self.instantiateVarAnon(method_var, .use_last_var); - - // Check all arguments - var i: u32 = 0; - while (i < args_span.span.len) : (i += 1) { - const arg_expr_idx = @as(CIR.Expr.Idx, @enumFromInt(args_span.span.start + i)); - does_fx = try self.checkExpr(arg_expr_idx) or does_fx; - } - - // Create argument list for the function call - var args = std.ArrayList(Var).init(self.gpa); - defer args.deinit(); - - // Add the receiver (the nominal type) as the first argument - try args.append(receiver_var); - - // Add the remaining arguments - i = 0; - while (i < args_span.span.len) : (i += 1) { - const arg_expr_idx = @as(CIR.Expr.Idx, @enumFromInt(args_span.span.start + i)); - const arg_var = @as(Var, @enumFromInt(@intFromEnum(arg_expr_idx))); - try args.append(arg_var); - } - - // Create a function type for the method call - const dot_access_var = @as(Var, @enumFromInt(@intFromEnum(expr_idx))); - const func_content = try self.types.mkFuncUnbound(args.items, dot_access_var); - const expected_func_var = try self.freshFromContent(func_content, expr_region); - - // Unify with the imported method type - _ = try self.unify(expected_func_var, method_instantiated); - - // Store the resolved method info for code generation - // This will be used by the code generator to emit the correct function call - // For now, the type information in the expression variable is sufficient - } else { - // Method not found in origin module - // TODO: Add a proper error type for method not found on nominal type - try self.types.setVarContent(@enumFromInt(@intFromEnum(expr_idx)), .err); - } - } else { - // Origin module not found - // TODO: Add a proper error type for origin module not found - try self.types.setVarContent(@enumFromInt(@intFromEnum(expr_idx)), .err); - } - } else { - // No arguments - this might be a field access on a nominal type's backing type - // TODO: Handle field access on nominal types - try self.types.setVarContent(@enumFromInt(@intFromEnum(expr_idx)), .err); - } + // Annotation-aware lambda type checking produces much better error + // messages, so first we have to determine if we have an expected + // type to validate against + const mb_expected_var: ?Var, const is_expected_from_anno: bool = blk: { + switch (expected) { + .no_expectation => break :blk .{ null, false }, + .expected => |expected_type| { + break :blk .{ expected_type.var_, expected_type.from_annotation }; }, - .record => |record| { - // Receiver is already a record, find the field - const fields = self.types.getRecordFieldsSlice(record.fields); - - // Find the field with the matching name - for (fields.items(.name), fields.items(.var_)) |field_name, field_var| { - if (field_name == dot_access.field_name) { - // Unify the dot access expression with the field type - const dot_access_var = @as(Var, @enumFromInt(@intFromEnum(expr_idx))); - _ = try self.unify(dot_access_var, field_var); - break; - } - } - }, - .record_unbound => |record_unbound| { - // Receiver is an unbound record, find the field - const fields = self.types.getRecordFieldsSlice(record_unbound); - - // Find the field with the matching name - for (fields.items(.name), fields.items(.var_)) |field_name, field_var| { - if (field_name == dot_access.field_name) { - // Unify the dot access expression with the field type - const dot_access_var = @as(Var, @enumFromInt(@intFromEnum(expr_idx))); - _ = try self.unify(dot_access_var, field_var); - break; - } - } - }, - else => { - // Receiver is not a record, this is a type error - // For now, we'll let unification handle the error - }, - }, - .flex_var => { - // Receiver is unbound, we need to constrain it to be a record with the field - // Create a fresh variable for the field type - const field_var = try self.fresh(expr_region); - const dot_access_var = @as(Var, @enumFromInt(@intFromEnum(expr_idx))); - _ = try self.unify(dot_access_var, field_var); - - // Create a record type with this field - const field_idx = try self.types.appendRecordField(.{ - .name = dot_access.field_name, - .var_ = field_var, - }); - const fields_range = types_mod.RecordField.SafeMultiList.Range{ - .start = field_idx, - .count = 1, - }; - - // Create an extension variable for other possible fields - // Create the record content - const record_content = types_mod.Content{ - .structure = .{ - .record_unbound = fields_range, - }, - }; - - // Unify the receiver with this record type - // - // TODO: Do we need to insert a CIR placeholder node here as well? - // What happens if later this type variable has a problem, and we - // try to look up it's region in CIR? - const record_var = try self.freshFromContent(record_content, expr_region); - - // Use the dot access expression as the constraint origin for better error reporting - _ = try self.unifyWithConstraintOrigin(receiver_var, record_var, dot_access_var); - - // Record that this variable was constrained by this dot access expression - try self.constraint_origins.put(receiver_var, dot_access_var); - // Constraint origin recorded for better error reporting - }, - else => { - // Other cases (rigid_var, alias, etc.) - let unification handle errors - }, - } - }, - .e_runtime_error => {}, - .e_crash => {}, - .e_dbg => {}, - .e_ellipsis => {}, - .e_expect => {}, - } - - return does_fx; -} - -// func // - -/// Helper function to unify a call expression with a function type -fn unifyFunctionCall( - self: *Self, - call_var: Var, // var for the entire call expr - call_func_var: Var, // var for the fn itself - call_args: []const CIR.Expr.Idx, // var for the fn args - expected_func: types_mod.Func, // the expected type of the fn (must be instantiated) - region: Region, - func_expr_idx: CIR.Expr.Idx, // the function expression for name extraction -) std.mem.Allocator.Error!bool { - // Extract function name if possible - const func_name: ?Ident.Idx = blk: { - const func_expr = self.cir.store.getExpr(func_expr_idx); - switch (func_expr) { - .e_lookup_local => |lookup| { - // Get the pattern that defines this identifier - const pattern = self.cir.store.getPattern(lookup.pattern_idx); - switch (pattern) { - .assign => |assign| break :blk assign.ident, - else => break :blk null, } - }, - else => break :blk null, - } - }; + }; - // Check arguments with expected types using bidirectional type checking - const expected_args = self.types.sliceVars(expected_func.args); - - // Only unify arguments if counts match - otherwise let the normal - // unification process handle the arity mismatch error - if (expected_args.len == call_args.len) { - // For function signatures with bound type variables, unify arguments with each other first - // This ensures proper error placement for cases like mk_pair("1", 2) where a, a -> Pair(a) - - // Find arguments that share the same type variable - for (expected_args, 0..) |expected_arg_1, i| { - const expected_resolved_1 = self.types.resolveVar(expected_arg_1); - - // Only check type variables, skip concrete types - if (expected_resolved_1.desc.content != .flex_var and expected_resolved_1.desc.content != .rigid_var) { - continue; - } - - // Look for other arguments with the same type variable - for (expected_args[i + 1 ..], i + 1..) |expected_arg_2, j| { - if (expected_arg_1 == expected_arg_2) { - // These two arguments are bound by the same type variable - unify them first - const arg_1 = @as(Var, @enumFromInt(@intFromEnum(call_args[i]))); - const arg_2 = @as(Var, @enumFromInt(@intFromEnum(call_args[j]))); - - const unify_result = try self.unify(arg_1, arg_2); - - if (unify_result.isProblem()) { - // Use the new error detail for bound type variable incompatibility - self.setProblemTypeMismatchDetail(unify_result.problem, .{ - .incompatible_fn_args_bound_var = .{ - .fn_name = func_name, - .first_arg_var = arg_1, - .second_arg_var = arg_2, - .first_arg_index = @intCast(i), - .second_arg_index = @intCast(j), - .num_args = @intCast(call_args.len), + // Then, even if we have an expected type, it may not actually be a function + const mb_expected_func: ?types_mod.Func = blk: { + if (mb_expected_var) |expected_var| { + // Here, we unwrap the function, following aliases, to get + // the actual function we want to check against + var var_ = expected_var; + var guard = types_mod.debug.IterationGuard.init("checkExpr.lambda.unwrapExpectedFunc"); + while (true) { + guard.tick(); + switch (self.types.resolveVar(var_).desc.content) { + .structure => |flat_type| { + switch (flat_type) { + .fn_pure => |func| break :blk func, + .fn_unbound => |func| break :blk func, + .fn_effectful => |func| break :blk func, + else => break :blk null, + } }, - }); - return false; // Early return on error + .alias => |alias| { + var_ = self.types.getAliasBackingVar(alias); + }, + else => break :blk null, + } + } + } else { + break :blk null; + } + }; + + { + // Enter the next rank + try env.var_pool.pushRank(); + defer env.var_pool.popRank(); + + // IMPORTANT: expr_var must be added to the new rank, not the + // outer rank + try self.setVarRank(expr_var, env); + + // Check the argument patterns + // This must happen *before* checking against the expected type so + // all the pattern types are inferred + const arg_pattern_idxs = self.cir.store.slicePatterns(lambda.args); + for (arg_pattern_idxs) |pattern_idx| { + try self.checkPattern(pattern_idx, env, .no_expectation); + } + + // Now, check if we have an expected function to validate against + if (mb_expected_func) |expected_func| { + const expected_func_args = self.types.sliceVars(expected_func.args); + + // Next, check if the arguments arities match + if (expected_func_args.len == arg_pattern_idxs.len) { + // If so, check each argument, passing in the expected type + + // First, find all the rigid variables in a the function's type + // and unify the matching corresponding lambda arguments together. + for (expected_func_args, 0..) |expected_arg_1, i| { + const expected_resolved_1 = self.types.resolveVar(expected_arg_1); + + // The expected type is an annotation and as such, + // should never contain a flex var. If it did, that + // would indicate that the annotation is malformed + // std.debug.assert(expected_resolved_1.desc.content != .flex); + + // Skip any concrete arguments + if (expected_resolved_1.desc.content != .rigid) { + continue; + } + + // Look for other arguments with the same type variable + for (expected_func_args[i + 1 ..], i + 1..) |expected_arg_2, j| for_blk: { + const expected_resolved_2 = self.types.resolveVar(expected_arg_2); + if (expected_resolved_1.var_ == expected_resolved_2.var_) { + // These two argument indexes in the called *function's* + // type have the same rigid variable! So, we unify + // the corresponding *lambda args* + + const arg_1 = @as(Var, ModuleEnv.varFrom(arg_pattern_idxs[i])); + const arg_2 = @as(Var, ModuleEnv.varFrom(arg_pattern_idxs[j])); + + const unify_result = try self.unify(arg_1, arg_2, env); + if (unify_result.isProblem()) { + // Use the new error detail for bound type variable incompatibility + self.setProblemTypeMismatchDetail(unify_result.problem, .{ + .incompatible_fn_args_bound_var = .{ + .fn_name = self.enclosing_func_name, + .first_arg_var = arg_1, + .second_arg_var = arg_2, + .first_arg_index = @intCast(i), + .second_arg_index = @intCast(j), + .num_args = @intCast(arg_pattern_idxs.len), + }, + }); + + // Stop execution + _ = try self.unifyWith(expr_var, .err, env); + break :for_blk; + } + } + } + } + + // Then, lastly, we unify the annotation types against the + // actual type + for (expected_func_args, arg_pattern_idxs) |expected_arg_var, pattern_idx| { + if (is_expected_from_anno) { + _ = try self.unifyWithCtx(expected_arg_var, ModuleEnv.varFrom(pattern_idx), env, .anno); + } else { + _ = try self.unify(expected_arg_var, ModuleEnv.varFrom(pattern_idx), env); + } + } + } else { + // This means the expected type and the actual lambda have + // an arity mismatch. This will be caught by the regular + // expectation checking code at the bottom of this function } } - } - } + const arg_vars: []Var = @ptrCast(arg_pattern_idxs); - // Apply constraint propagation for numeric literals with concrete types - for (expected_args, call_args) |expected_arg, arg_expr_idx| { - const expected_resolved = self.types.resolveVar(expected_arg); - - // Only apply constraint propagation for concrete types, not type variables - if (expected_resolved.desc.content != .flex_var and expected_resolved.desc.content != .rigid_var) { - const arg_expr = self.cir.store.getExpr(arg_expr_idx); - - if (arg_expr == .e_int) { - const literal_var = @as(Var, @enumFromInt(@intFromEnum(arg_expr_idx))); - _ = try self.unify(literal_var, expected_arg); - } - } - } - - // Regular unification using the argument vars - var arg_index: u32 = 0; - for (expected_args, call_args) |expected_arg, arg_expr_idx| { - const actual_arg = @as(Var, @enumFromInt(@intFromEnum(arg_expr_idx))); - - // Instantiate polymorphic arguments before unification - var arg_to_unify = actual_arg; - if (self.types.needsInstantiation(actual_arg)) { - arg_to_unify = try self.instantiateVarAnon(actual_arg, .{ .explicit = region }); - } - - const arg_result = try self.unify(expected_arg, arg_to_unify); - self.setDetailIfTypeMismatch(arg_result, .{ - .incompatible_fn_call_arg = .{ - .fn_name = null, // Get function name for better error message - .arg_var = actual_arg, - .incompatible_arg_index = arg_index, - .num_args = @intCast(call_args.len), - }, - }); - arg_index += 1; - } - // The call's type is the instantiated return type - _ = try self.unify(call_var, expected_func.ret); - } else { - // Arity mismatch - arguments already checked - - // Fall back to normal unification to get proper error message - // Use the original func_var to avoid issues with instantiated variables in error reporting - const actual_arg_vars: []Var = @constCast(@ptrCast(@alignCast(call_args))); - const func_content = try self.types.mkFuncUnbound(actual_arg_vars, call_var); - const expected_func_var = try self.freshFromContent(func_content, region); - _ = try self.unify(call_func_var, expected_func_var); - } - - return false; -} - -/// Performs bidirectional type checking for lambda expressions with optional type annotations. -/// -/// ## Constraint Ordering -/// -/// Parameter constraints from annotations are applied BEFORE checking the lambda body. -/// This ensures rigid variables are properly constrained during body analysis, causing -/// type errors to surface at their actual source rather than during later unification. -/// -/// **Why this ordering matters:** -/// ``` -/// Pair(a) := [Pair(a, a)] -/// mk_pair_invalid : a, b -> Pair(a) -/// mk_pair_invalid = |x, y| Pair.Pair(x, y) -/// -/// // Step 1: Constrain parameters from annotation first -/// x := a[rigid], y := b[rigid] -/// -/// // Step 2: Check body - error detected immediately at constructor -/// Pair.Pair(x, y) // ERROR: Pair requires same type, but x=a[rigid], y=b[rigid] -/// ``` -/// -/// Without upfront constraints, the error would only surface during final annotation -/// validation, making it harder to locate the actual problem in the source code. -/// Similar to checkLambdaWithAnno but only validates return type, not the entire function. -/// This is used for lambdas to preserve error locations while still catching type errors. -fn checkLambdaForClosure( - self: *Self, - expr_idx: CIR.Expr.Idx, - lambda: std.meta.FieldType(CIR.Expr, .e_lambda), - anno_type: ?Var, -) std.mem.Allocator.Error!bool { - const trace = tracy.trace(@src()); - defer trace.end(); - - // Get the actual lambda arguments - const arg_patterns = self.cir.store.slicePatterns(lambda.args); - const arg_vars: []Var = @ptrCast(@alignCast(arg_patterns)); - - var mb_expected_func: ?types_mod.Func = null; - - // STEP 1: Apply annotation constraints to parameters FIRST - if (anno_type) |anno_var| { - const expected_resolved = self.types.resolveVar(anno_var); - mb_expected_func = switch (expected_resolved.desc.content) { - .structure => |s| switch (s) { - .fn_pure, .fn_effectful, .fn_unbound => |func| func, - else => null, - }, - else => null, - }; - - if (mb_expected_func) |func| { - const expected_args = self.types.sliceVars(func.args); - if (expected_args.len == arg_patterns.len) { - // Constrain parameters with annotation types - for (arg_patterns, expected_args) |pattern_idx, expected_arg| { - // Check the pattern - try self.checkPattern(pattern_idx); - - // Unify against the annotation - const pattern_var = ModuleEnv.varFrom(pattern_idx); - _ = try self.unify(pattern_var, expected_arg); - } - } - } - } - - // STEP 2: Check the body with return type constraint propagation - const is_effectful = if (mb_expected_func) |func| - try self.checkExprWithExpectedAndAnnotation(lambda.body, func.ret, true) - else - try self.checkExpr(lambda.body); - - // STEP 3: Build the function type - const fn_var = ModuleEnv.varFrom(expr_idx); - const return_var = @as(Var, @enumFromInt(@intFromEnum(lambda.body))); - - // Ensure the return variable has the correct region for error reporting - const body_region = self.cir.store.getExprRegion(lambda.body); - try self.fillInRegionsThrough(return_var); - self.setRegionAt(return_var, body_region); - - if (is_effectful) { - _ = try self.types.setVarContent(fn_var, try self.types.mkFuncEffectful(arg_vars, return_var)); - } else { - _ = try self.types.setVarContent(fn_var, try self.types.mkFuncUnbound(arg_vars, return_var)); - } - - // STEP 4: Don't validate here - let checkDef do the final unification - // This preserves error locations correctly - - // NOTE: We don't do the final unification with the full annotation here - // The caller (checkDef) will handle that to preserve error locations - - return is_effectful; -} - -fn checkLambdaWithAnno( - self: *Self, - expr_idx: CIR.Expr.Idx, - _: Region, - lambda: std.meta.FieldType(CIR.Expr, .e_lambda), - anno_type: ?Var, -) std.mem.Allocator.Error!bool { - const trace = tracy.trace(@src()); - defer trace.end(); - - // Get the actual lambda arguments - const arg_patterns = self.cir.store.slicePatterns(lambda.args); - const arg_vars: []Var = @ptrCast(@alignCast(arg_patterns)); - - var mb_expected_var: ?Var = null; - var mb_expected_func: ?types_mod.Func = null; - - // STEP 1: Apply annotation constraints to parameters FIRST - if (anno_type) |anno_var| { - mb_expected_var = anno_var; - - const expected_resolved = self.types.resolveVar(anno_var); - mb_expected_func = switch (expected_resolved.desc.content) { - .structure => |s| switch (s) { - .fn_pure, .fn_effectful, .fn_unbound => |func| func, - else => null, - }, - else => null, - }; - - if (mb_expected_func) |func| { - const expected_args = self.types.sliceVars(func.args); - if (expected_args.len == arg_patterns.len) { - // Constrain parameters with annotation types - for (arg_patterns, expected_args) |pattern_idx, expected_arg| { - // Check the pattern - try self.checkPattern(pattern_idx); - - // Unify against the annotation - const pattern_var = ModuleEnv.varFrom(pattern_idx); - _ = try self.unify(pattern_var, expected_arg); - } - } - } - } - - // STEP 2: Check the body with return type constraint propagation for literals - const is_effectful = if (mb_expected_func) |func| - try self.checkExprWithExpectedAndAnnotation(lambda.body, func.ret, true) - else - try self.checkExpr(lambda.body); - - // STEP 3: Build the function type - const fn_var = ModuleEnv.varFrom(expr_idx); - const return_var = @as(Var, @enumFromInt(@intFromEnum(lambda.body))); - - // Ensure the return variable has the correct region for error reporting - const body_region = self.cir.store.getExprRegion(lambda.body); - try self.fillInRegionsThrough(return_var); - self.setRegionAt(return_var, body_region); - - if (is_effectful) { - _ = try self.types.setVarContent(fn_var, try self.types.mkFuncEffectful(arg_vars, return_var)); - } else { - _ = try self.types.setVarContent(fn_var, try self.types.mkFuncUnbound(arg_vars, return_var)); - } - - // STEP 4: Validate the function body against the annotation return type - if (mb_expected_func) |func| { - _ = try self.unify(return_var, func.ret); - } - - // STEP 5: Validate the entire function against the annotation - if (mb_expected_var) |expected_var| { - _ = try self.unifyWithAnnotation(fn_var, expected_var); - } - - return is_effectful; -} - -// binop // - -/// Check the types for a binary operation expression -fn checkBinopExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_region: Region, binop: CIR.Expr.Binop, expected_type: ?Var, from_annotation: bool) Allocator.Error!bool { - const trace = tracy.trace(@src()); - defer trace.end(); - - switch (binop.op) { - .add, .sub, .mul, .div, .rem, .pow, .div_trunc => { - // Check operands first - var does_fx = try self.checkExpr(binop.lhs); - does_fx = try self.checkExpr(binop.rhs) or does_fx; - - // For now, we'll constrain both operands to be numbers - // In the future, this will use static dispatch based on the lhs type - const lhs_var = @as(Var, @enumFromInt(@intFromEnum(binop.lhs))); - const rhs_var = @as(Var, @enumFromInt(@intFromEnum(binop.rhs))); - const result_var = @as(Var, @enumFromInt(@intFromEnum(expr_idx))); - - // For bidirectional type checking: if we have an expected type, - // we need to unify all operands and result with it. - // This ensures literals like `2` in `|x| x + 2` get properly constrained - // when the lambda has a type annotation like `I64 -> I64`. - if (expected_type) |expected| { - // All three must be the same type for arithmetic operations - if (from_annotation) { - _ = try self.unifyWithAnnotation(lhs_var, expected); - _ = try self.unifyWithAnnotation(rhs_var, expected); - _ = try self.unifyWithAnnotation(result_var, expected); + // Check the the body of the expr + // If we have an expected function, use that as the expr's expected type + // Also track the return type so `return` expressions can use it + const saved_return_type = self.enclosing_func_return_type; + if (mb_expected_func) |expected_func| { + self.enclosing_func_return_type = expected_func.ret; + does_fx = try self.checkExpr(lambda.body, env, .{ + .expected = .{ .var_ = expected_func.ret, .from_annotation = is_expected_from_anno }, + }) or does_fx; } else { - _ = try self.unify(lhs_var, expected); - _ = try self.unify(rhs_var, expected); - _ = try self.unify(result_var, expected); + // When no expected type, the body's type becomes the return type. + // We need a fresh var so early returns can unify with it. + const body_var = ModuleEnv.varFrom(lambda.body); + self.enclosing_func_return_type = body_var; + does_fx = try self.checkExpr(lambda.body, env, .no_expectation) or does_fx; } - } else { - // No expected type - use fresh number variables to maintain polymorphism - const num_content = Content{ .structure = .{ .num = .{ .num_unbound = .{ .sign_needed = false, .bits_needed = 0 } } } }; - const num_var_lhs = try self.freshFromContent(num_content, expr_region); - const num_var_rhs = try self.freshFromContent(num_content, expr_region); - const num_var_result = try self.freshFromContent(num_content, expr_region); + self.enclosing_func_return_type = saved_return_type; + const body_var = ModuleEnv.varFrom(lambda.body); - // Unify lhs, rhs, and result with the number type - _ = try self.unify(num_var_lhs, lhs_var); - _ = try self.unify(num_var_rhs, rhs_var); - _ = try self.unify(result_var, num_var_result); + // Unify all early returns with the body's return type. + // This ensures that `return x` has the same type as the implicit return. + try self.unifyEarlyReturns(lambda.body, body_var, env); + + // Create the function type + if (does_fx) { + _ = try self.unifyWith(expr_var, try self.types.mkFuncEffectful(arg_vars, body_var), env); + } else { + _ = try self.unifyWith(expr_var, try self.types.mkFuncUnbound(arg_vars, body_var), env); + } + + // Now that we are existing the scope, we must generalize then pop this rank + try self.generalizer.generalize(self.gpa, &env.var_pool, env.rank()); + + // Check any accumulated static dispatch constraints + try self.checkDeferredStaticDispatchConstraints(env); } - return does_fx; + // Note that so far, we have not yet unified against the + // annotation's effectfulness/pureness. This is intentional! + // Below this large switch statement, there's the regular expr + // <-> expected unification. This will catch any difference in + // effectfullness, and it'll link the root expected var with the + // expr_var + }, - .lt, .gt, .le, .ge, .eq, .ne => { - // Check operands first - var does_fx = try self.checkExpr(binop.lhs); - does_fx = try self.checkExpr(binop.rhs) or does_fx; - - // For equality/comparison, operands must be the same type - const lhs_var = @as(Var, @enumFromInt(@intFromEnum(binop.lhs))); - const rhs_var = @as(Var, @enumFromInt(@intFromEnum(binop.rhs))); - _ = try self.unify(lhs_var, rhs_var); - - // Comparison operators always return Bool - const expr_var = @as(Var, @enumFromInt(@intFromEnum(expr_idx))); - const fresh_bool = try self.instantiateVarAnon(ModuleEnv.varFrom(can.Can.BUILTIN_BOOL_TYPE), .{ .explicit = expr_region }); - _ = try self.unify(expr_var, fresh_bool); - - return does_fx; + .e_closure => |closure| { + does_fx = try self.checkExpr(closure.lambda_idx, env, expected) or does_fx; + _ = try self.unify(expr_var, ModuleEnv.varFrom(closure.lambda_idx), env); }, - .@"and" => { - var does_fx = try self.checkExpr(binop.lhs); - does_fx = try self.checkExpr(binop.rhs) or does_fx; + // function calling // + .e_call => |call| { + switch (call.called_via) { + .apply => blk: { + // First, check the function being called + // It could be effectful, e.g. `(mk_fn!())(arg)` + does_fx = try self.checkExpr(call.func, env, .no_expectation) or does_fx; + const func_var = ModuleEnv.varFrom(call.func); - const lhs_fresh_bool = try self.instantiateVarAnon(ModuleEnv.varFrom(can.Can.BUILTIN_BOOL_TYPE), .{ .explicit = expr_region }); - const lhs_result = try self.unify(lhs_fresh_bool, @enumFromInt(@intFromEnum(binop.lhs))); - self.setDetailIfTypeMismatch(lhs_result, .{ .invalid_bool_binop = .{ - .binop_expr = expr_idx, - .problem_side = .lhs, - .binop = .@"and", - } }); + // Resolve the func var + const resolved_func = self.types.resolveVar(func_var).desc.content; + var did_err = resolved_func == .err; - if (lhs_result.isOk()) { - const rhs_fresh_bool = try self.instantiateVarAnon(ModuleEnv.varFrom(can.Can.BUILTIN_BOOL_TYPE), .{ .explicit = expr_region }); - const rhs_result = try self.unify(rhs_fresh_bool, @enumFromInt(@intFromEnum(binop.rhs))); - self.setDetailIfTypeMismatch(rhs_result, .{ .invalid_bool_binop = .{ - .binop_expr = expr_idx, - .problem_side = .rhs, - .binop = .@"and", - } }); - } + // Second, check the arguments being called + // It could be effectful, e.g. `fn(mk_arg!())` + const call_arg_expr_idxs = self.cir.store.sliceExpr(call.args); + for (call_arg_expr_idxs) |call_arg_idx| { + does_fx = try self.checkExpr(call_arg_idx, env, .no_expectation) or does_fx; - return does_fx; - }, - .@"or" => { - var does_fx = try self.checkExpr(binop.lhs); - does_fx = try self.checkExpr(binop.rhs) or does_fx; + // Check if this arg errored + did_err = did_err or (self.types.resolveVar(ModuleEnv.varFrom(call_arg_idx)).desc.content == .err); + } - const lhs_fresh_bool = try self.instantiateVarAnon(ModuleEnv.varFrom(can.Can.BUILTIN_BOOL_TYPE), .{ .explicit = expr_region }); - const lhs_result = try self.unify(lhs_fresh_bool, @enumFromInt(@intFromEnum(binop.lhs))); - self.setDetailIfTypeMismatch(lhs_result, .{ .invalid_bool_binop = .{ - .binop_expr = expr_idx, - .problem_side = .lhs, - .binop = .@"or", - } }); + if (did_err) { + // If the fn or any args had error, propgate the error + // without doing any additional work + try self.unifyWith(expr_var, .err, env); + } else { + // From the base function type, extract the actual function info + // and also track whether the function is effectful + const FuncInfo = struct { func: types_mod.Func, is_effectful: bool }; + const mb_func_info: ?FuncInfo = inner_blk: { + // Here, we unwrap the function, following aliases, to get + // the actual function we want to check against + var var_ = func_var; + var guard = types_mod.debug.IterationGuard.init("checkExpr.call.unwrapFuncVar"); + while (true) { + guard.tick(); + switch (self.types.resolveVar(var_).desc.content) { + .structure => |flat_type| { + switch (flat_type) { + .fn_pure => |func| break :inner_blk FuncInfo{ .func = func, .is_effectful = false }, + .fn_unbound => |func| break :inner_blk FuncInfo{ .func = func, .is_effectful = false }, + .fn_effectful => |func| break :inner_blk FuncInfo{ .func = func, .is_effectful = true }, + else => break :inner_blk null, + } + }, + .alias => |alias| { + var_ = self.types.getAliasBackingVar(alias); + }, + else => break :inner_blk null, + } + } + }; + const mb_func = if (mb_func_info) |info| info.func else null; - if (lhs_result.isOk()) { - const rhs_fresh_bool = try self.instantiateVarAnon(ModuleEnv.varFrom(can.Can.BUILTIN_BOOL_TYPE), .{ .explicit = expr_region }); - const rhs_result = try self.unify(rhs_fresh_bool, @enumFromInt(@intFromEnum(binop.rhs))); - self.setDetailIfTypeMismatch(rhs_result, .{ .invalid_bool_binop = .{ - .binop_expr = expr_idx, - .problem_side = .rhs, - .binop = .@"or", - } }); - } + // If the function being called is effectful, mark this expression as effectful + if (mb_func_info) |info| { + if (info.is_effectful) { + does_fx = true; + } + } - return does_fx; - }, - .pipe_forward => { - var does_fx = try self.checkExpr(binop.lhs); - does_fx = try self.checkExpr(binop.rhs) or does_fx; - return does_fx; - }, - .null_coalesce => { - var does_fx = try self.checkExpr(binop.lhs); - does_fx = try self.checkExpr(binop.rhs) or does_fx; - return does_fx; - }, - } -} + // Get the name of the function (for error messages) + const func_name: ?Ident.Idx = inner_blk: { + const func_expr = self.cir.store.getExpr(call.func); + switch (func_expr) { + .e_lookup_local => |lookup| { + // Get the pattern that defines this identifier + const pattern = self.cir.store.getPattern(lookup.pattern_idx); + switch (pattern) { + .assign => |assign| break :inner_blk assign.ident, + else => break :inner_blk null, + } + }, + else => break :inner_blk null, + } + }; -fn checkUnaryMinusExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_region: Region, unary: CIR.Expr.UnaryMinus) Allocator.Error!bool { - const trace = tracy.trace(@src()); - defer trace.end(); + // Now, check the call args against the type of function + if (mb_func) |func| { + const func_args = self.types.sliceVars(func.args); - // Check the operand expression - const does_fx = try self.checkExpr(unary.expr); + if (func_args.len == call_arg_expr_idxs.len) { + // First, find all the "rigid" variables in a the function's type + // and unify the matching corresponding call arguments together. + // + // Here, "rigid" is in quotes because at this point, the expected function + // has been instantiated such that the rigid variables should all resolve + // to the same exact flex variable. So we are actually checking for flex + // variables here. + for (func_args, 0..) |expected_arg_1, i| { + const expected_resolved_1 = self.types.resolveVar(expected_arg_1); - // For unary minus, we constrain the operand and result to be numbers - const operand_var = @as(Var, @enumFromInt(@intFromEnum(unary.expr))); - const result_var = @as(Var, @enumFromInt(@intFromEnum(expr_idx))); + // Ensure the above comment is true. That is, that all + // rigid vars for this function have been instantiated to + // flex vars by the time we get here. + // std.debug.assert(expected_resolved_1.desc.content != .rigid); - // Create a fresh number variable for the operation - const num_content = Content{ .structure = .{ .num = .{ .num_unbound = .{ .sign_needed = true, .bits_needed = 0 } } } }; - const num_var = try self.freshFromContent(num_content, expr_region); + // Skip any concrete arguments + if (expected_resolved_1.desc.content != .flex and expected_resolved_1.desc.content != .rigid) { + continue; + } - // Unify operand and result with the number type - _ = try self.unify(operand_var, num_var); - _ = try self.unify(result_var, num_var); + // Look for other arguments with the same type variable + for (func_args[i + 1 ..], i + 1..) |expected_arg_2, j| { + const expected_resolved_2 = self.types.resolveVar(expected_arg_2); + if (expected_resolved_1.var_ == expected_resolved_2.var_) { + // These two argument indexes in the called *function's* + // type have the same rigid variable! So, we unify + // the corresponding *call args* - return does_fx; -} + const arg_1 = @as(Var, ModuleEnv.varFrom(call_arg_expr_idxs[i])); + const arg_2 = @as(Var, ModuleEnv.varFrom(call_arg_expr_idxs[j])); -fn checkUnaryNotExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_region: Region, unary: CIR.Expr.UnaryNot) Allocator.Error!bool { - const trace = tracy.trace(@src()); - defer trace.end(); + const unify_result = try self.unify(arg_1, arg_2, env); + if (unify_result.isProblem()) { + // Use the new error detail for bound type variable incompatibility + self.setProblemTypeMismatchDetail(unify_result.problem, .{ + .incompatible_fn_args_bound_var = .{ + .fn_name = func_name, + .first_arg_var = arg_1, + .second_arg_var = arg_2, + .first_arg_index = @intCast(i), + .second_arg_index = @intCast(j), + .num_args = @intCast(call_arg_expr_idxs.len), + }, + }); - // Check the operand expression - const does_fx = try self.checkExpr(unary.expr); + // Stop execution + _ = try self.unifyWith(expr_var, .err, env); + break :blk; + } + } + } + } - // For unary not, we constrain the operand and result to be booleans - const operand_var = @as(Var, @enumFromInt(@intFromEnum(unary.expr))); - const result_var = @as(Var, @enumFromInt(@intFromEnum(expr_idx))); + // Check the function's arguments against the actual + // called arguments, unifying each one + for (func_args, call_arg_expr_idxs, 0..) |expected_arg_var, call_expr_idx, arg_index| { + const unify_result = try self.unify(expected_arg_var, ModuleEnv.varFrom(call_expr_idx), env); + if (unify_result.isProblem()) { + // Use the new error detail for bound type variable incompatibility + self.setProblemTypeMismatchDetail(unify_result.problem, .{ + .incompatible_fn_call_arg = .{ + .fn_name = func_name, + .arg_var = ModuleEnv.varFrom(call_expr_idx), + .incompatible_arg_index = @intCast(arg_index), + .num_args = @intCast(call_arg_expr_idxs.len), + }, + }); - // Create a fresh boolean variable for the operation - const bool_var = try self.instantiateVarAnon(ModuleEnv.varFrom(can.Can.BUILTIN_BOOL_TYPE), .{ .explicit = expr_region }); + // Stop execution + _ = try self.unifyWith(expr_var, .err, env); + break :blk; + } + } - // Unify operand and result with the boolean type - _ = try self.unify(operand_var, bool_var); - _ = try self.unify(result_var, bool_var); + // Redirect the expr to the function's return type + _ = try self.unify(expr_var, func.ret, env); + } else { + // Arity mismatch - the function expects a different + // number of arguments than were provided + const fn_snapshot = try self.snapshots.snapshotVarForError(self.types, &self.type_writer, func_var); + _ = try self.problems.appendProblem(self.cir.gpa, .{ + .fn_call_arity_mismatch = .{ + .fn_name = func_name, + .fn_var = func_var, + .fn_snapshot = fn_snapshot, + .call_region = expr_region, + .expected_args = @intCast(func_args.len), + .actual_args = @intCast(call_arg_expr_idxs.len), + }, + }); - return does_fx; -} + // Set the expression to error type + try self.unifyWith(expr_var, .err, env); + } + } else { + // We get here if the type of expr being called + // (`mk_fn` in `(mk_fn())(arg)`) is NOT already + // inferred to be a function type. -// nominal // + // This can mean a regular type mismatch, but it can also + // mean that the thing being called yet has not yet been + // inferred (like if this is an anonymous function param) -// nominal // + // Either way, we know what the type *should* be, based + // on how it's being used here. So we create that func + // type and unify the function being called against it -/// Check that a nominal type constructor usage is valid and perform type inference. -/// -/// This function validates that a user's constructor application (like `Maybe.Just(1)`) -/// is compatible with the nominal type's definition and infers the concrete types. -/// -/// Example: -/// Maybe a := [Just(a), None] -/// val = Maybe.Just(1) // This call validates Just(1) against Maybe's definition -/// -/// Parameters: -/// * `node_var` - Variable representing the entire expression (initially a placeholder). -/// This will be redirected to the final inferred type. -/// * `node_region` - Source region of the entire expression for error reporting -/// * `node_backing_var` - Variable for the constructor application's internal type (e.g., `Just(1)`) -/// * `node_backing_type` - Kind of nominal backing (tag, record, tuple, or val) -/// * `real_nominal_var` - Variable of the nominal type declaration (e.g., `Maybe a`) -fn checkNominal( - self: *Self, - node_var: Var, - node_region: Region, - node_backing_var: Var, - node_backing_type: CIR.Expr.NominalBackingType, - real_nominal_var: Var, -) Allocator.Error!void { - // Clear the rigid variable substitution map to ensure fresh instantiation - self.anonymous_rigid_var_subs.items.clearRetainingCapacity(); + const call_arg_vars: []Var = @ptrCast(call_arg_expr_idxs); + const call_func_ret = try self.fresh(env, expr_region); + const call_func_content = try self.types.mkFuncUnbound(call_arg_vars, call_func_ret); + const call_func_var = try self.freshFromContent(call_func_content, env, expr_region); - // Algorithm overview: - // 1. Validate that the nominal type exists and is well-formed - // 2. Instantiate the nominal type's backing type with fresh variables - // 3. Unify the instantiated backing type against the user's constructor - // 4. If unification succeeds, instantiate the nominal type and assign to node_var - // 5. If unification fails, report appropriate error and mark node_var as error - // - // The key insight is using a shared rigid variable map so that type parameters - // in the backing type and nominal type get the same substitutions. + _ = try self.unify(func_var, call_func_var, env); - // Step 1: Extract and validate the nominal type - const nominal_content = self.types.resolveVar(real_nominal_var).desc.content; - const nominal_type = switch (nominal_content) { - .structure => |structure| switch (structure) { - .nominal_type => |nt| nt, - else => { - // The supposed nominal type is actually some other structure - try self.types.setVarContent(node_var, .err); - return; - }, - }, - else => { - // The nominal type is in an error state or unresolved - try self.types.setVarContent(node_var, .err); - return; - }, - }; - - // Step 2: Instantiate the nominal type's backing type - // This converts rigid type variables (like `a[r]`) to fresh flexible variables - // that can be unified. The backing type defines the structure (e.g., [Just(a), None]) - const nominal_backing_var = self.types.getNominalBackingVar(nominal_type); - const instantiated_backing_var = try self.instantiateVar( - nominal_backing_var, - &self.anonymous_rigid_var_subs, - .{ .explicit = node_region }, - ); - - // TODO: If we can, unify arguments directly to get better error messages - // Unifying by the whole thing works, but doesn't have great error messages - // always - - // Step 3: Unify the instantiated backing type with the user's constructor - // This checks if `Just(1)` is compatible with `[Just(a), None]` and if so, - // unifies `a` with the type of `1` (e.g., Num(_size)) - const result = try self.unify(instantiated_backing_var, node_backing_var); - - // Step 4: Handle the unification result - switch (result) { - .ok => { - // Unification succeeded - the constructor is valid for this nominal type - // Now instantiate the full nominal type using the same rigid variable map - // so it gets the same type substitutions as the backing type - const instantiated_qualified_var = try self.instantiateVar( - real_nominal_var, - &self.anonymous_rigid_var_subs, - .{ .explicit = node_region }, - ); - - // Assign the inferred concrete type to the expression variable - // After this, `node_var` will resolve to something like `Maybe(Num(_size))` - try self.types.setVarRedirect(node_var, instantiated_qualified_var); - }, - .problem => |problem_idx| { - // Unification failed - the constructor is incompatible with the nominal type - // Set a specific error message based on the backing type kind - switch (node_backing_type) { - .tag => { - // Constructor doesn't exist or has wrong arity/types - self.setProblemTypeMismatchDetail(problem_idx, .invalid_nominal_tag); + // Then, we set the root expr to redirect to the return + // type of that function, since a call expr ultimate + // resolve to the returned type + _ = try self.unify(expr_var, call_func_ret, env); + } + } }, else => { - // TODO: Add specific error messages for records, tuples, etc. + // The canonicalizer currently only produces CalledVia.apply for e_call expressions. + // Other call types (binop, unary_op, string_interpolation, record_builder) are + // represented as different expression types. If we hit this, there's a compiler bug. + std.debug.assert(false); + try self.unifyWith(expr_var, .err, env); }, } - - // Mark the entire expression as having a type error - try self.types.setVarContent(node_var, .err); }, + .e_if => |if_expr| { + does_fx = try self.checkIfElseExpr(expr_idx, expr_region, env, if_expr) or does_fx; + }, + .e_match => |match| { + does_fx = try self.checkMatchExpr(expr_idx, env, match) or does_fx; + }, + .e_binop => |binop| { + does_fx = try self.checkBinopExpr(expr_idx, expr_region, env, binop, expected) or does_fx; + }, + .e_unary_minus => |unary| { + does_fx = try self.checkUnaryMinusExpr(expr_idx, expr_region, env, unary) or does_fx; + }, + .e_unary_not => |unary| { + does_fx = try self.checkUnaryNotExpr(expr_idx, expr_region, env, unary) or does_fx; + }, + .e_dot_access => |dot_access| { + // Dot access can either indicate record access or static dispatch + + // Check the receiver expression + // E.g. thing.val + // ^^^^^ + does_fx = try self.checkExpr(dot_access.receiver, env, .no_expectation) or does_fx; + const receiver_var = ModuleEnv.varFrom(dot_access.receiver); + + if (dot_access.args) |dispatch_args| { + // If this dot access has args, then it's static dispatch + + // Resolve the receiver var + const resolved_receiver = self.types.resolveVar(receiver_var); + var did_err = resolved_receiver.desc.content == .err; + + // Check the args + // E.g. thing.dispatch(a, b) + // ^ ^ + const dispatch_arg_expr_idxs = self.cir.store.sliceExpr(dispatch_args); + for (dispatch_arg_expr_idxs) |dispatch_arg_expr_idx| { + does_fx = try self.checkExpr(dispatch_arg_expr_idx, env, .no_expectation) or does_fx; + + // Check if this arg errored + did_err = did_err or (self.types.resolveVar(ModuleEnv.varFrom(dispatch_arg_expr_idx)).desc.content == .err); + } + + if (did_err) { + // If the receiver or any arguments are errors, then + // propgate the error without doing any static dispatch work + try self.unifyWith(expr_var, .err, env); + } else { + // For static dispatch to be used like `thing.dispatch(...)` the + // method being dispatched on must accept the type of `thing` as + // it's first arg. So, we prepend the `receiver_var` to the args list + const first_arg_range = try self.types.appendVars(&.{receiver_var}); + const rest_args_range = try self.types.appendVars(@ptrCast(dispatch_arg_expr_idxs)); + const dispatch_arg_vars_range = Var.SafeList.Range{ + .start = first_arg_range.start, + .count = rest_args_range.count + 1, + }; + + // Since the return type of this dispatch is unknown, create a + // flex to represent it + const dispatch_ret_var = try self.fresh(env, expr_region); + + // Now, create the function being dispatched + // Use field_name_region so error messages point at the method name, not the whole expression + const constraint_fn_var = try self.freshFromContent(.{ .structure = .{ .fn_unbound = Func{ + .args = dispatch_arg_vars_range, + .ret = dispatch_ret_var, + .needs_instantiation = false, + } } }, env, dot_access.field_name_region); + + // Then, create the static dispatch constraint + const constraint = StaticDispatchConstraint{ + .fn_name = dot_access.field_name, + .fn_var = constraint_fn_var, + .origin = .method_call, + }; + const constraint_range = try self.types.appendStaticDispatchConstraints(&.{constraint}); + + // Create our constrained flex, and unify it with the receiver + // Use field_name_region so error messages point at the method name, not the whole expression + const constrained_var = try self.freshFromContent( + .{ .flex = Flex{ .name = null, .constraints = constraint_range } }, + env, + dot_access.field_name_region, + ); + + _ = try self.unify(constrained_var, receiver_var, env); + + // Then, set the root expr to redirect to the ret var + _ = try self.unify(expr_var, dispatch_ret_var, env); + } + } else { + // Otherwise, this is dot access on a record + + // Create a type for the inferred type of this record access + // E.g. foo.bar -> { bar: flex } a + const record_field_var = try self.fresh(env, expr_region); + const record_field_range = try self.types.appendRecordFields(&.{types_mod.RecordField{ + .name = dot_access.field_name, + .var_ = record_field_var, + }}); + const record_being_accessed = try self.freshFromContent(.{ .structure = .{ + .record_unbound = record_field_range, + } }, env, expr_region); + + // Then, unify the actual receiver type with the expected record + _ = try self.unify(record_being_accessed, receiver_var, env); + _ = try self.unify(expr_var, record_field_var, env); + } + }, + .e_crash => { + try self.unifyWith(expr_var, .{ .flex = Flex.init() }, env); + }, + .e_dbg => |dbg| { + // dbg evaluates its inner expression but returns {} (like expect) + _ = try self.checkExpr(dbg.expr, env, .no_expectation); + does_fx = true; + try self.unifyWith(expr_var, .{ .structure = .empty_record }, env); + }, + .e_expect => |expect| { + does_fx = try self.checkExpr(expect.body, env, expected) or does_fx; + try self.unifyWith(expr_var, .{ .structure = .empty_record }, env); + }, + .e_for => |for_expr| { + // Check the pattern + try self.checkPattern(for_expr.patt, env, .no_expectation); + const for_ptrn_var: Var = ModuleEnv.varFrom(for_expr.patt); + + // Check the list expression + does_fx = try self.checkExpr(for_expr.expr, env, .no_expectation) or does_fx; + const for_expr_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(for_expr.expr)); + const for_expr_var: Var = ModuleEnv.varFrom(for_expr.expr); + + // Check that the expr is list of the ptrn + const list_content = try self.mkListContent(for_ptrn_var, env); + const list_var = try self.freshFromContent(list_content, env, for_expr_region); + _ = try self.unify(list_var, for_expr_var, env); + + // Check the body + does_fx = try self.checkExpr(for_expr.body, env, .no_expectation) or does_fx; + const for_body_var: Var = ModuleEnv.varFrom(for_expr.body); + + // Check that the for body evaluates to {} + const body_ret = try self.freshFromContent(.{ .structure = .empty_record }, env, for_expr_region); + _ = try self.unify(body_ret, for_body_var, env); + + // The for expression itself evaluates to {} + try self.unifyWith(expr_var, .{ .structure = .empty_record }, env); + }, + .e_ellipsis => { + try self.unifyWith(expr_var, .{ .flex = Flex.init() }, env); + }, + .e_anno_only => { + // For annotation-only expressions, the type comes from the annotation. + // This case should only occur when the expression has an annotation (which is + // enforced during canonicalization), so the expected type should be set. + switch (expected) { + .no_expectation => { + // This shouldn't happen since we always create e_anno_only with an annotation + try self.unifyWith(expr_var, .err, env); + }, + .expected => |expected_type| { + // Redirect expr_var to the annotation var so that lookups get the correct type + _ = try self.unify(expr_var, expected_type.var_, env); + }, + } + }, + .e_return => |ret| { + // Early return expression - check the inner expression against enclosing function's return type + // If we're inside a function, use its return type. Otherwise fall back to expected. + const return_expected: Expected = if (self.enclosing_func_return_type) |ret_var| + .{ .expected = .{ .var_ = ret_var, .from_annotation = false } } + else + expected; + does_fx = try self.checkExpr(ret.expr, env, return_expected) or does_fx; + // e_return "never returns" - it exits the function, so it can unify with any expected type. + // This allows it to be used in if branches alongside other expressions. + switch (expected) { + .expected => |exp| { + _ = try self.unify(expr_var, exp.var_, env); + }, + .no_expectation => { + // No expected type, leave expr_var as a flex var + }, + } + }, + .e_hosted_lambda => { + // For hosted lambda expressions, the type comes from the annotation. + // This is similar to e_anno_only - the implementation is provided by the host. + switch (expected) { + .no_expectation => { + // This shouldn't happen since hosted lambdas always have annotations + try self.unifyWith(expr_var, .err, env); + }, + .expected => |expected_type| { + // Redirect expr_var to the annotation var so that lookups get the correct type + _ = try self.unify(expr_var, expected_type.var_, env); + }, + } + }, + .e_low_level_lambda => |ll| { + // For low-level lambda expressions, treat like a lambda with a crash body. + // Check the body (which will be e_runtime_error or similar) + does_fx = try self.checkExpr(ll.body, env, .no_expectation) or does_fx; + + // The lambda's type comes from the annotation. + // Like e_anno_only, this should always have an annotation. + // The type will be unified with the expected type in the code below. + switch (expected) { + .no_expectation => unreachable, + .expected => { + // The expr_var will be unified with the annotation var below + }, + } + }, + .e_type_var_dispatch => |tvd| { + // Type variable dispatch expression: Thing.method(args) where Thing is a type var alias. + // This is similar to static dispatch (e_dot_access with args) but dispatches on a + // type variable rather than on the type of a receiver expression. + + // Check the args and track errors + const dispatch_arg_expr_idxs = self.cir.store.exprSlice(tvd.args); + var did_err = false; + for (dispatch_arg_expr_idxs) |dispatch_arg_expr_idx| { + does_fx = try self.checkExpr(dispatch_arg_expr_idx, env, .no_expectation) or does_fx; + did_err = did_err or (self.types.resolveVar(ModuleEnv.varFrom(dispatch_arg_expr_idx)).desc.content == .err); + } + + if (did_err) { + // If any arguments are errors, propagate the error + try self.unifyWith(expr_var, .err, env); + } else { + // Get the type var alias statement to access the type variable + const type_var_alias_stmt = self.cir.store.getStatement(tvd.type_var_alias_stmt); + const type_var_anno = type_var_alias_stmt.s_type_var_alias.type_var_anno; + const type_var = ModuleEnv.varFrom(type_var_anno); + + // For type var dispatch, the arguments are just the explicit args (no receiver) + const dispatch_arg_vars_range = try self.types.appendVars(@ptrCast(dispatch_arg_expr_idxs)); + + // Since the return type of this dispatch is unknown, create a flex to represent it + const dispatch_ret_var = try self.fresh(env, expr_region); + + // Create the function being dispatched + const constraint_fn_var = try self.freshFromContent(.{ .structure = .{ .fn_unbound = Func{ + .args = dispatch_arg_vars_range, + .ret = dispatch_ret_var, + .needs_instantiation = false, + } } }, env, expr_region); + + // Create the static dispatch constraint + const constraint = StaticDispatchConstraint{ + .fn_name = tvd.method_name, + .fn_var = constraint_fn_var, + .origin = .method_call, + }; + const constraint_range = try self.types.appendStaticDispatchConstraints(&.{constraint}); + + // Create a constrained flex and unify it with the type variable + const constrained_var = try self.freshFromContent( + .{ .flex = Flex{ .name = null, .constraints = constraint_range } }, + env, + expr_region, + ); + + _ = try self.unify(constrained_var, type_var, env); + + // Set the expression type to the return type of the dispatch + _ = try self.unify(expr_var, dispatch_ret_var, env); + } + }, + .e_runtime_error => { + try self.unifyWith(expr_var, .err, env); + }, + } + + // If we were provided with an expected type, unify against it + switch (expected) { + .no_expectation => {}, + .expected => |expected_type| { + if (expected_type.from_annotation) { + _ = try self.unifyWithCtx(expected_type.var_, expr_var, env, .anno); + } else { + _ = try self.unify(expected_type.var_, expr_var, env); + } + }, + } + + return does_fx; +} + +// stmts // + +const BlockStatementsResult = struct { + does_fx: bool, + diverges: bool, +}; + +/// Given a slice of stmts, type check each one +/// Returns whether any statement has effects and whether the block diverges (return/crash) +fn checkBlockStatements(self: *Self, statements: []const CIR.Statement.Idx, env: *Env, _: Region) std.mem.Allocator.Error!BlockStatementsResult { + var does_fx = false; + var diverges = false; + for (statements) |stmt_idx| { + const stmt = self.cir.store.getStatement(stmt_idx); + const stmt_var = ModuleEnv.varFrom(stmt_idx); + const stmt_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(stmt_idx)); + + try self.setVarRank(stmt_var, env); + + switch (stmt) { + .s_decl => |decl_stmt| { + // Check the pattern + try self.checkPattern(decl_stmt.pattern, env, .no_expectation); + const decl_pattern_var: Var = ModuleEnv.varFrom(decl_stmt.pattern); + + // Extract function name from the pattern (for better error messages) + const saved_func_name = self.enclosing_func_name; + self.enclosing_func_name = inner_blk: { + const pattern = self.cir.store.getPattern(decl_stmt.pattern); + switch (pattern) { + .assign => |assign| break :inner_blk assign.ident, + else => break :inner_blk null, + } + }; + defer self.enclosing_func_name = saved_func_name; + + // Evaluate the rhs of the expression + const decl_expr_var: Var = ModuleEnv.varFrom(decl_stmt.expr); + { + // Enter a new rank + try env.var_pool.pushRank(); + defer env.var_pool.popRank(); + + // Check the annotation, if it exists + const expectation = blk: { + if (decl_stmt.anno) |annotation_idx| { + // Generate the annotation type var in-place + try self.generateAnnotationType(annotation_idx, env); + const annotation_var = ModuleEnv.varFrom(annotation_idx); + + // TODO: If we instantiate here, then var lookups break. But if we don't + // then the type anno gets corrupted if we have an error in the body + // const instantiated_anno_var = try self.instantiateVarPreserveRigids( + // annotation_var, + // rank, + // .use_last_var, + // ); + + // Return the expectation + break :blk Expected{ + .expected = .{ .var_ = annotation_var, .from_annotation = true }, + }; + } else { + break :blk Expected.no_expectation; + } + }; + + does_fx = try self.checkExpr(decl_stmt.expr, env, expectation) or does_fx; + + // Now that we are existing the scope, we must generalize then pop this rank + try self.generalizer.generalize(self.gpa, &env.var_pool, env.rank()); + + // Check any accumulated static dispatch constraints + try self.checkDeferredStaticDispatchConstraints(env); + } + + _ = try self.unify(decl_pattern_var, decl_expr_var, env); + + // Unify the pattern with the expression + + _ = try self.unify(stmt_var, decl_pattern_var, env); + }, + .s_decl_gen => |decl_stmt| { + // Generalized declarations (let-polymorphism) - handled same as s_decl for type checking + // Check the pattern + try self.checkPattern(decl_stmt.pattern, env, .no_expectation); + const decl_pattern_var: Var = ModuleEnv.varFrom(decl_stmt.pattern); + + // Extract function name from the pattern (for better error messages) + const saved_func_name = self.enclosing_func_name; + self.enclosing_func_name = inner_blk: { + const pattern = self.cir.store.getPattern(decl_stmt.pattern); + switch (pattern) { + .assign => |assign| break :inner_blk assign.ident, + else => break :inner_blk null, + } + }; + defer self.enclosing_func_name = saved_func_name; + // Evaluate the rhs of the expression + const decl_expr_var: Var = ModuleEnv.varFrom(decl_stmt.expr); + { + // Enter a new rank + try env.var_pool.pushRank(); + defer env.var_pool.popRank(); + + // Check the annotation, if it exists + const expectation = blk: { + if (decl_stmt.anno) |annotation_idx| { + // Generate the annotation type var in-place + try self.generateAnnotationType(annotation_idx, env); + const annotation_var = ModuleEnv.varFrom(annotation_idx); + + // Return the expectation + break :blk Expected{ + .expected = .{ .var_ = annotation_var, .from_annotation = true }, + }; + } else { + break :blk Expected.no_expectation; + } + }; + + does_fx = try self.checkExpr(decl_stmt.expr, env, expectation) or does_fx; + + // Now that we are existing the scope, we must generalize then pop this rank + try self.generalizer.generalize(self.gpa, &env.var_pool, env.rank()); + + // Check any accumulated static dispatch constraints + try self.checkDeferredStaticDispatchConstraints(env); + } + + _ = try self.unify(decl_pattern_var, decl_expr_var, env); + + // Unify the pattern with the expression + _ = try self.unify(stmt_var, decl_pattern_var, env); + }, + .s_var => |var_stmt| { + // Check the pattern + try self.checkPattern(var_stmt.pattern_idx, env, .no_expectation); + const reassign_pattern_var: Var = ModuleEnv.varFrom(var_stmt.pattern_idx); + + does_fx = try self.checkExpr(var_stmt.expr, env, .no_expectation) or does_fx; + const var_expr: Var = ModuleEnv.varFrom(var_stmt.expr); + + // Unify the pattern with the expression + _ = try self.unify(reassign_pattern_var, var_expr, env); + + _ = try self.unify(stmt_var, var_expr, env); + }, + .s_reassign => |reassign| { + // We don't need to check the pattern here since it was already + // checked when this var was created. + // + // try self.checkPattern(reassign.pattern_idx, env, .no_expectation); + + const reassign_pattern_var: Var = ModuleEnv.varFrom(reassign.pattern_idx); + + does_fx = try self.checkExpr(reassign.expr, env, .no_expectation) or does_fx; + const reassign_expr_var: Var = ModuleEnv.varFrom(reassign.expr); + + // Unify the pattern with the expression + _ = try self.unify(reassign_pattern_var, reassign_expr_var, env); + + _ = try self.unify(stmt_var, reassign_expr_var, env); + }, + .s_for => |for_stmt| { + // Check the pattern + // for item in [1,2,3] { + // ^^^^ + try self.checkPattern(for_stmt.patt, env, .no_expectation); + const for_ptrn_var: Var = ModuleEnv.varFrom(for_stmt.patt); + + // Check the expr + // for item in [1,2,3] { + // ^^^^^^^ + does_fx = try self.checkExpr(for_stmt.expr, env, .no_expectation) or does_fx; + const for_expr_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(for_stmt.expr)); + const for_expr_var: Var = ModuleEnv.varFrom(for_stmt.expr); + + // Check that the expr is list of the ptrn + const list_content = try self.mkListContent(for_ptrn_var, env); + const list_var = try self.freshFromContent(list_content, env, for_expr_region); + _ = try self.unify(list_var, for_expr_var, env); + + // Check the body + // for item in [1,2,3] { + // print!(item.toStr()) <<<< + // } + does_fx = try self.checkExpr(for_stmt.body, env, .no_expectation) or does_fx; + const for_body_var: Var = ModuleEnv.varFrom(for_stmt.body); + + // Check that the for body evaluates to {} + const body_ret = try self.freshFromContent(.{ .structure = .empty_record }, env, for_expr_region); + _ = try self.unify(body_ret, for_body_var, env); + + _ = try self.unify(stmt_var, for_body_var, env); + }, + .s_while => |while_stmt| { + // Check the condition + // while $count < 10 { + // ^^^^^^^^^^^ + does_fx = try self.checkExpr(while_stmt.cond, env, .no_expectation) or does_fx; + const cond_var: Var = ModuleEnv.varFrom(while_stmt.cond); + const cond_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(while_stmt.cond)); + + // Check that condition is Bool + const bool_var = try self.freshBool(env, cond_region); + _ = try self.unify(bool_var, cond_var, env); + + // Check the body + // while $count < 10 { + // print!($count.toStr()) <<<< + // $count = $count + 1 + // } + does_fx = try self.checkExpr(while_stmt.body, env, .no_expectation) or does_fx; + const while_body_var: Var = ModuleEnv.varFrom(while_stmt.body); + + // Check that the while body evaluates to {} + const body_ret = try self.freshFromContent(.{ .structure = .empty_record }, env, cond_region); + _ = try self.unify(body_ret, while_body_var, env); + + _ = try self.unify(stmt_var, while_body_var, env); + }, + .s_expr => |expr| { + does_fx = try self.checkExpr(expr.expr, env, .no_expectation) or does_fx; + const expr_var: Var = ModuleEnv.varFrom(expr.expr); + + const resolved = self.types.resolveVar(expr_var).desc.content; + const is_empty_record = blk: { + if (resolved == .err) break :blk true; + if (resolved != .structure) break :blk false; + switch (resolved.structure) { + .empty_record => break :blk true, + .record => |record| { + // A record is effectively empty if it has no fields + const fields_slice = self.types.getRecordFieldsSlice(record.fields); + break :blk fields_slice.len == 0; + }, + else => break :blk false, + } + }; + if (!is_empty_record) { + const snapshot = try self.snapshots.snapshotVarForError(self.types, &self.type_writer, expr_var); + _ = try self.problems.appendProblem(self.cir.gpa, .{ .unused_value = .{ + .var_ = expr_var, + .snapshot = snapshot, + } }); + } + + _ = try self.unify(stmt_var, expr_var, env); + }, + .s_dbg => |expr| { + does_fx = try self.checkExpr(expr.expr, env, .no_expectation) or does_fx; + const expr_var: Var = ModuleEnv.varFrom(expr.expr); + + _ = try self.unify(stmt_var, expr_var, env); + }, + .s_expect => |expr_stmt| { + does_fx = try self.checkExpr(expr_stmt.body, env, .no_expectation) or does_fx; + const body_var: Var = ModuleEnv.varFrom(expr_stmt.body); + + const bool_var = try self.freshBool(env, stmt_region); + _ = try self.unify(bool_var, body_var, env); + + _ = try self.unify(stmt_var, body_var, env); + }, + .s_crash => |_| { + try self.unifyWith(stmt_var, .{ .flex = Flex.init() }, env); + diverges = true; + }, + .s_return => |ret| { + // Type check the return expression + does_fx = try self.checkExpr(ret.expr, env, .no_expectation) or does_fx; + + // A return statement's type should be a flex var so it can unify with any type. + // This allows branches containing early returns to match any other branch type. + // The actual unification with the function return type happens in unifyEarlyReturns. + try self.unifyWith(stmt_var, .{ .flex = Flex.init() }, env); + diverges = true; + }, + .s_import, .s_alias_decl, .s_nominal_decl, .s_type_anno => { + // These are only valid at the top level, czer reports error + try self.unifyWith(stmt_var, .err, env); + }, + .s_type_var_alias => { + // Type var alias introduces no new constraints during type checking + // The alias is already registered in scope by canonicalization + // The type var it references is a rigid var from the enclosing function + try self.unifyWith(stmt_var, .{ .structure = .empty_record }, env); + }, + .s_runtime_error => { + try self.unifyWith(stmt_var, .err, env); + }, + } + } + return .{ .does_fx = does_fx, .diverges = diverges }; +} + +/// Traverse an expression to find s_return statements and unify them with the expected return type. +/// This is called after type-checking a lambda body to ensure all early returns have matching types. +fn unifyEarlyReturns(self: *Self, expr_idx: CIR.Expr.Idx, return_var: Var, env: *Env) std.mem.Allocator.Error!void { + const expr = self.cir.store.getExpr(expr_idx); + switch (expr) { + .e_block => |block| { + // Check all statements in the block for returns + for (self.cir.store.sliceStatements(block.stmts)) |stmt_idx| { + try self.unifyEarlyReturnsInStmt(stmt_idx, return_var, env); + } + // Also recurse into the final expression + try self.unifyEarlyReturns(block.final_expr, return_var, env); + }, + .e_if => |if_expr| { + // Check all branches + for (self.cir.store.sliceIfBranches(if_expr.branches)) |branch_idx| { + const branch = self.cir.store.getIfBranch(branch_idx); + try self.unifyEarlyReturns(branch.body, return_var, env); + } + // Check the final else + try self.unifyEarlyReturns(if_expr.final_else, return_var, env); + }, + .e_match => |match| { + // Check all branches + for (self.cir.store.sliceMatchBranches(match.branches)) |branch_idx| { + const branch = self.cir.store.getMatchBranch(branch_idx); + try self.unifyEarlyReturns(branch.value, return_var, env); + } + }, + .e_for => |for_expr| { + // Check the list expression and body for returns + try self.unifyEarlyReturns(for_expr.expr, return_var, env); + try self.unifyEarlyReturns(for_expr.body, return_var, env); + }, + // Lambdas create a new scope for returns - don't recurse into them + .e_lambda, .e_closure => {}, + // All other expressions don't contain statements + else => {}, + } +} + +/// Check a statement for s_return and unify with the expected return type. +fn unifyEarlyReturnsInStmt(self: *Self, stmt_idx: CIR.Statement.Idx, return_var: Var, env: *Env) std.mem.Allocator.Error!void { + const stmt = self.cir.store.getStatement(stmt_idx); + switch (stmt) { + .s_return => |ret| { + const return_expr_var = ModuleEnv.varFrom(ret.expr); + _ = try self.unify(return_expr_var, return_var, env); + }, + .s_decl => |decl| { + // Recurse into the declaration's expression + try self.unifyEarlyReturns(decl.expr, return_var, env); + }, + .s_decl_gen => |decl| { + // Recurse into the generalized declaration's expression + try self.unifyEarlyReturns(decl.expr, return_var, env); + }, + .s_var => |var_stmt| { + // Recurse into the var's expression + try self.unifyEarlyReturns(var_stmt.expr, return_var, env); + }, + .s_reassign => |reassign| { + try self.unifyEarlyReturns(reassign.expr, return_var, env); + }, + .s_for => |for_stmt| { + try self.unifyEarlyReturns(for_stmt.expr, return_var, env); + try self.unifyEarlyReturns(for_stmt.body, return_var, env); + }, + .s_while => |while_stmt| { + try self.unifyEarlyReturns(while_stmt.cond, return_var, env); + try self.unifyEarlyReturns(while_stmt.body, return_var, env); + }, + .s_expr => |s| { + // Recurse into the expression (could contain blocks with returns) + try self.unifyEarlyReturns(s.expr, return_var, env); + }, + .s_expect => |s| { + try self.unifyEarlyReturns(s.body, return_var, env); + }, + .s_dbg => |s| { + try self.unifyEarlyReturns(s.expr, return_var, env); + }, + // These statements don't contain expressions with potential returns + .s_crash, .s_import, .s_alias_decl, .s_nominal_decl, .s_type_anno, .s_type_var_alias, .s_runtime_error => {}, } } @@ -2059,7 +4335,8 @@ fn checkIfElseExpr( self: *Self, if_expr_idx: CIR.Expr.Idx, expr_region: Region, - if_: std.meta.FieldType(CIR.Expr, .e_if), + env: *Env, + if_: @FieldType(CIR.Expr, @tagName(.e_if)), ) std.mem.Allocator.Error!bool { const trace = tracy.trace(@src()); defer trace.end(); @@ -2074,17 +4351,17 @@ fn checkIfElseExpr( const first_branch = self.cir.store.getIfBranch(first_branch_idx); // Check the condition of the 1st branch - var does_fx = try self.checkExpr(first_branch.cond); - const first_cond_var: Var = @enumFromInt(@intFromEnum(first_branch.cond)); - const bool_var = try self.instantiateVarAnon(ModuleEnv.varFrom(can.Can.BUILTIN_BOOL_TYPE), .{ .explicit = expr_region }); - const first_cond_result = try self.unify(bool_var, first_cond_var); + var does_fx = try self.checkExpr(first_branch.cond, env, .no_expectation); + const first_cond_var: Var = ModuleEnv.varFrom(first_branch.cond); + const bool_var = try self.freshBool(env, expr_region); + const first_cond_result = try self.unify(bool_var, first_cond_var, env); self.setDetailIfTypeMismatch(first_cond_result, .incompatible_if_cond); // Then we check the 1st branch's body - does_fx = try self.checkExpr(first_branch.body) or does_fx; + does_fx = try self.checkExpr(first_branch.body, env, .no_expectation) or does_fx; // The 1st branch's body is the type all other branches must match - const branch_var = @as(Var, @enumFromInt(@intFromEnum(first_branch.body))); + const branch_var = @as(Var, ModuleEnv.varFrom(first_branch.body)); // Total number of branches (including final else) const num_branches: u32 = @intCast(branches.len + 1); @@ -2094,16 +4371,16 @@ fn checkIfElseExpr( const branch = self.cir.store.getIfBranch(branch_idx); // Check the branches condition - does_fx = try self.checkExpr(branch.cond) or does_fx; - const cond_var: Var = @enumFromInt(@intFromEnum(branch.cond)); - const branch_bool_var = try self.instantiateVarAnon(ModuleEnv.varFrom(can.Can.BUILTIN_BOOL_TYPE), .{ .explicit = expr_region }); - const cond_result = try self.unify(branch_bool_var, cond_var); + does_fx = try self.checkExpr(branch.cond, env, .no_expectation) or does_fx; + const cond_var: Var = ModuleEnv.varFrom(branch.cond); + const branch_bool_var = try self.freshBool(env, expr_region); + const cond_result = try self.unify(branch_bool_var, cond_var, env); self.setDetailIfTypeMismatch(cond_result, .incompatible_if_cond); // Check the branch body - does_fx = try self.checkExpr(branch.body) or does_fx; - const body_var: Var = @enumFromInt(@intFromEnum(branch.body)); - const body_result = try self.unify(branch_var, body_var); + does_fx = try self.checkExpr(branch.body, env, .no_expectation) or does_fx; + const body_var: Var = ModuleEnv.varFrom(branch.body); + const body_result = try self.unify(branch_var, body_var, env); self.setDetailIfTypeMismatch(body_result, problem.TypeMismatchDetail{ .incompatible_if_branches = .{ .parent_if_expr = if_expr_idx, .last_if_branch = last_if_branch, @@ -2116,15 +4393,15 @@ fn checkIfElseExpr( for (branches[cur_index + 1 ..]) |remaining_branch_idx| { const remaining_branch = self.cir.store.getIfBranch(remaining_branch_idx); - does_fx = try self.checkExpr(remaining_branch.cond) or does_fx; - const remaining_cond_var: Var = @enumFromInt(@intFromEnum(remaining_branch.cond)); + does_fx = try self.checkExpr(remaining_branch.cond, env, .no_expectation) or does_fx; + const remaining_cond_var: Var = ModuleEnv.varFrom(remaining_branch.cond); - const fresh_bool = try self.instantiateVarAnon(ModuleEnv.varFrom(can.Can.BUILTIN_BOOL_TYPE), .{ .explicit = expr_region }); - const remaining_cond_result = try self.unify(fresh_bool, remaining_cond_var); + const fresh_bool = try self.freshBool(env, expr_region); + const remaining_cond_result = try self.unify(fresh_bool, remaining_cond_var, env); self.setDetailIfTypeMismatch(remaining_cond_result, .incompatible_if_cond); - does_fx = try self.checkExpr(remaining_branch.body) or does_fx; - try self.types.setVarContent(@enumFromInt(@intFromEnum(remaining_branch.body)), .err); + does_fx = try self.checkExpr(remaining_branch.body, env, .no_expectation) or does_fx; + try self.unifyWith(ModuleEnv.varFrom(remaining_branch.body), .err, env); } // Break to avoid cascading errors @@ -2135,9 +4412,9 @@ fn checkIfElseExpr( } // Check the final else - does_fx = try self.checkExpr(if_.final_else) or does_fx; - const final_else_var: Var = @enumFromInt(@intFromEnum(if_.final_else)); - const final_else_result = try self.unify(branch_var, final_else_var); + does_fx = try self.checkExpr(if_.final_else, env, .no_expectation) or does_fx; + const final_else_var: Var = ModuleEnv.varFrom(if_.final_else); + const final_else_result = try self.unify(branch_var, final_else_var, env); self.setDetailIfTypeMismatch(final_else_result, problem.TypeMismatchDetail{ .incompatible_if_branches = .{ .parent_if_expr = if_expr_idx, .last_if_branch = last_if_branch, @@ -2145,9 +4422,9 @@ fn checkIfElseExpr( .problem_branch_index = num_branches - 1, } }); - // Unify the if expression's type variable with the branch type - const if_expr_var: Var = @enumFromInt(@intFromEnum(if_expr_idx)); - _ = try self.unify(if_expr_var, branch_var); + // Set the entire expr's type to be the type of the branch + const if_expr_var: Var = ModuleEnv.varFrom(if_expr_idx); + _ = try self.unify(if_expr_var, branch_var, env); return does_fx; } @@ -2155,17 +4432,16 @@ fn checkIfElseExpr( // match // /// Check the types for an if-else expr -fn checkMatchExpr(self: *Self, expr_idx: CIR.Expr.Idx, match: CIR.Expr.Match) Allocator.Error!bool { +fn checkMatchExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, match: CIR.Expr.Match) Allocator.Error!bool { const trace = tracy.trace(@src()); defer trace.end(); // Check the match's condition - var does_fx = try self.checkExpr(match.cond); + var does_fx = try self.checkExpr(match.cond, env, .no_expectation); const cond_var = ModuleEnv.varFrom(match.cond); - // Bail if we somehow have 0 branches - // TODO: Should this be an error? Here or in Can? - if (match.branches.span.len == 0) return does_fx; + // Assert we have at least 1 branch + std.debug.assert(match.branches.span.len > 0); // Get slice of branches const branch_idxs = self.cir.store.sliceMatchBranches(match.branches); @@ -2175,27 +4451,22 @@ fn checkMatchExpr(self: *Self, expr_idx: CIR.Expr.Idx, match: CIR.Expr.Match) Al // against. const first_branch_idx = branch_idxs[0]; const first_branch = self.cir.store.getMatchBranch(first_branch_idx); - const first_branch_ptrn_idxs = self.cir.store.sliceMatchBranchPatterns(first_branch.patterns); - for (first_branch_ptrn_idxs, 0..) |branch_ptrn_idx, cur_ptrn_index| { + for (first_branch_ptrn_idxs) |branch_ptrn_idx| { const branch_ptrn = self.cir.store.getMatchBranchPattern(branch_ptrn_idx); - try self.checkPattern(branch_ptrn.pattern); + try self.checkPattern(branch_ptrn.pattern, env, .no_expectation); const branch_ptrn_var = ModuleEnv.varFrom(branch_ptrn.pattern); - const ptrn_result = try self.unify(cond_var, branch_ptrn_var); - self.setDetailIfTypeMismatch(ptrn_result, problem.TypeMismatchDetail{ .incompatible_match_patterns = .{ + const ptrn_result = try self.unify(cond_var, branch_ptrn_var, env); + self.setDetailIfTypeMismatch(ptrn_result, problem.TypeMismatchDetail{ .incompatible_match_cond_pattern = .{ .match_expr = expr_idx, - .num_branches = @intCast(match.branches.span.len), - .problem_branch_index = 0, - .num_patterns = @intCast(first_branch_ptrn_idxs.len), - .problem_pattern_index = @intCast(cur_ptrn_index), } }); } // Check the first branch's value, then use that at the branch_var - does_fx = try self.checkExpr(first_branch.value) or does_fx; - const branch_var = ModuleEnv.varFrom(first_branch.value); + does_fx = try self.checkExpr(first_branch.value, env, .no_expectation) or does_fx; + const val_var = ModuleEnv.varFrom(first_branch.value); // Then iterate over the rest of the branches for (branch_idxs[1..], 1..) |branch_idx, branch_cur_index| { @@ -2206,11 +4477,11 @@ fn checkMatchExpr(self: *Self, expr_idx: CIR.Expr.Idx, match: CIR.Expr.Match) Al for (branch_ptrn_idxs, 0..) |branch_ptrn_idx, cur_ptrn_index| { // Check the pattern's sub types const branch_ptrn = self.cir.store.getMatchBranchPattern(branch_ptrn_idx); - try self.checkPattern(branch_ptrn.pattern); + try self.checkPattern(branch_ptrn.pattern, env, .no_expectation); // Check the pattern against the cond const branch_ptrn_var = ModuleEnv.varFrom(branch_ptrn.pattern); - const ptrn_result = try self.unify(cond_var, branch_ptrn_var); + const ptrn_result = try self.unify(cond_var, branch_ptrn_var, env); self.setDetailIfTypeMismatch(ptrn_result, problem.TypeMismatchDetail{ .incompatible_match_patterns = .{ .match_expr = expr_idx, .num_branches = @intCast(match.branches.span.len), @@ -2221,8 +4492,8 @@ fn checkMatchExpr(self: *Self, expr_idx: CIR.Expr.Idx, match: CIR.Expr.Match) Al } // Then, check the body - does_fx = try self.checkExpr(branch.value) or does_fx; - const branch_result = try self.unify(branch_var, ModuleEnv.varFrom(branch.value)); + does_fx = try self.checkExpr(branch.value, env, .no_expectation) or does_fx; + const branch_result = try self.unify(val_var, ModuleEnv.varFrom(branch.value), env); self.setDetailIfTypeMismatch(branch_result, problem.TypeMismatchDetail{ .incompatible_match_branches = .{ .match_expr = expr_idx, .num_branches = @intCast(match.branches.span.len), @@ -2240,11 +4511,11 @@ fn checkMatchExpr(self: *Self, expr_idx: CIR.Expr.Idx, match: CIR.Expr.Match) Al for (other_branch_ptrn_idxs, 0..) |other_branch_ptrn_idx, other_cur_ptrn_index| { // Check the pattern's sub types const other_branch_ptrn = self.cir.store.getMatchBranchPattern(other_branch_ptrn_idx); - try self.checkPattern(other_branch_ptrn.pattern); + try self.checkPattern(other_branch_ptrn.pattern, env, .no_expectation); // Check the pattern against the cond const other_branch_ptrn_var = ModuleEnv.varFrom(other_branch_ptrn.pattern); - const ptrn_result = try self.unify(cond_var, other_branch_ptrn_var); + const ptrn_result = try self.unify(cond_var, other_branch_ptrn_var, env); self.setDetailIfTypeMismatch(ptrn_result, problem.TypeMismatchDetail{ .incompatible_match_patterns = .{ .match_expr = expr_idx, .num_branches = @intCast(match.branches.span.len), @@ -2255,8 +4526,8 @@ fn checkMatchExpr(self: *Self, expr_idx: CIR.Expr.Idx, match: CIR.Expr.Match) Al } // Then check the other branch's exprs - does_fx = try self.checkExpr(other_branch.value) or does_fx; - try self.types.setVarContent(ModuleEnv.varFrom(other_branch.value), .err); + does_fx = try self.checkExpr(other_branch.value, env, .no_expectation) or does_fx; + try self.unifyWith(ModuleEnv.varFrom(other_branch.value), .err, env); } // Then stop type checking for this branch @@ -2264,6 +4535,423 @@ fn checkMatchExpr(self: *Self, expr_idx: CIR.Expr.Idx, match: CIR.Expr.Match) Al } } + // Unify the root expr with the match value + _ = try self.unify(ModuleEnv.varFrom(expr_idx), val_var, env); + + return does_fx; +} + +// unary minus // + +fn checkUnaryMinusExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_region: Region, env: *Env, unary: CIR.Expr.UnaryMinus) Allocator.Error!bool { + const trace = tracy.trace(@src()); + defer trace.end(); + + // Check the operand expression + const does_fx = try self.checkExpr(unary.expr, env, .no_expectation); + + const expr_var = ModuleEnv.varFrom(expr_idx); + const operand_var = @as(Var, ModuleEnv.varFrom(unary.expr)); + + // Desugar -a to a.negate() + // Get the negate identifier + const method_name = self.cir.idents.negate; + + // Create the function type: operand_type -> ret_type + const args_range = try self.types.appendVars(&.{operand_var}); + + // The return type is unknown, so create a fresh variable + const ret_var = try self.fresh(env, expr_region); + try env.var_pool.addVarToRank(ret_var, env.rank()); + + // Create the constraint function type + const constraint_fn_var = try self.freshFromContent(.{ .structure = .{ .fn_unbound = Func{ + .args = args_range, + .ret = ret_var, + .needs_instantiation = false, + } } }, env, expr_region); + try env.var_pool.addVarToRank(constraint_fn_var, env.rank()); + + // Create the static dispatch constraint + const constraint = StaticDispatchConstraint{ + .fn_name = method_name, + .fn_var = constraint_fn_var, + .origin = .desugared_binop, + }; + const constraint_range = try self.types.appendStaticDispatchConstraints(&.{constraint}); + + // Create a constrained flex and unify it with the operand + const constrained_var = try self.freshFromContent( + .{ .flex = Flex{ .name = null, .constraints = constraint_range } }, + env, + expr_region, + ); + try env.var_pool.addVarToRank(constrained_var, env.rank()); + + _ = try self.unify(constrained_var, operand_var, env); + + // Set the expression to redirect to the return type + _ = try self.unify(expr_var, ret_var, env); + + return does_fx; +} + +// unary not // + +fn checkUnaryNotExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_region: Region, env: *Env, unary: CIR.Expr.UnaryNot) Allocator.Error!bool { + const trace = tracy.trace(@src()); + defer trace.end(); + + const expr_var = @as(Var, ModuleEnv.varFrom(expr_idx)); + + // Check the operand expression + const does_fx = try self.checkExpr(unary.expr, env, .no_expectation); + + // For unary not, we constrain the operand and result to be booleans + const operand_var = @as(Var, ModuleEnv.varFrom(unary.expr)); + + // Create a fresh boolean variable for the operation + const bool_var = try self.freshBool(env, expr_region); + + // Unify result with the boolean type + _ = try self.unify(bool_var, operand_var, env); + + // Redirect the result to the boolean type + _ = try self.unify(expr_var, bool_var, env); + + return does_fx; +} + +// binop // + +/// Check the types for a binary operation expression +fn checkBinopExpr( + self: *Self, + expr_idx: CIR.Expr.Idx, + expr_region: Region, + env: *Env, + binop: CIR.Expr.Binop, + expected: Expected, +) Allocator.Error!bool { + const trace = tracy.trace(@src()); + defer trace.end(); + + const expr_var = ModuleEnv.varFrom(expr_idx); + const lhs_var = @as(Var, ModuleEnv.varFrom(binop.lhs)); + const rhs_var = @as(Var, ModuleEnv.varFrom(binop.rhs)); + + // Check operands first + var does_fx = false; + does_fx = try self.checkExpr(binop.lhs, env, .no_expectation) or does_fx; + does_fx = try self.checkExpr(binop.rhs, env, .no_expectation) or does_fx; + + switch (binop.op) { + .add => { + // For builtin numeric types, use the efficient special-cased numeric constraint logic + // For user-defined nominal types, desugar `a + b` to `a.plus(b)` using static dispatch + + // Check if lhs is a nominal type + const lhs_resolved = self.types.resolveVar(lhs_var).desc.content; + const is_nominal = switch (lhs_resolved) { + .structure => |s| s == .nominal_type, + else => false, + }; + + if (is_nominal) { + // User-defined nominal type: use static dispatch to call the plus method + // Get the pre-cached "plus" identifier from the ModuleEnv + const method_name = self.cir.idents.plus; + + // Unify lhs and rhs to ensure both operands have the same type + const unify_result = try self.unify(lhs_var, rhs_var, env); + + // If unification failed, short-circuit and set the expression to error + if (!unify_result.isOk()) { + try self.unifyWith(expr_var, .err, env); + return does_fx; + } + + // Create the function type: lhs_type, rhs_type -> ret_type + const args_range = try self.types.appendVars(&.{ lhs_var, rhs_var }); + + // The return type is unknown, so create a fresh variable + const ret_var = try self.fresh(env, expr_region); + + // Create the constraint function type + const constraint_fn_var = try self.freshFromContent(.{ .structure = .{ .fn_unbound = Func{ + .args = args_range, + .ret = ret_var, + .needs_instantiation = false, + } } }, env, expr_region); + + // Create the static dispatch constraint + const constraint = StaticDispatchConstraint{ + .fn_name = method_name, + .fn_var = constraint_fn_var, + .origin = .desugared_binop, + }; + const constraint_range = try self.types.appendStaticDispatchConstraints(&.{constraint}); + + // Create a constrained flex and unify it with the lhs (receiver) + const constrained_var = try self.freshFromContent( + .{ .flex = Flex{ .name = null, .constraints = constraint_range } }, + env, + expr_region, + ); + + _ = try self.unify(constrained_var, lhs_var, env); + + // Set the expression to redirect to the return type + _ = try self.unify(expr_var, ret_var, env); + } else { + // Builtin numeric type: use standard numeric constraints + // This is the same as the other arithmetic operators + switch (expected) { + .expected => |expectation| { + const lhs_instantiated = try self.instantiateVar(expectation.var_, env, .{ .explicit = expr_region }); + const rhs_instantiated = try self.instantiateVar(expectation.var_, env, .{ .explicit = expr_region }); + + if (expectation.from_annotation) { + _ = try self.unifyWithCtx(lhs_instantiated, lhs_var, env, .anno); + _ = try self.unifyWithCtx(rhs_instantiated, rhs_var, env, .anno); + } else { + _ = try self.unify(lhs_instantiated, lhs_var, env); + _ = try self.unify(rhs_instantiated, rhs_var, env); + } + }, + .no_expectation => { + // No expectation - operand types will be inferred + // The unification of lhs and rhs below will ensure they're the same type + }, + } + + // Unify left and right together + const unify_result = try self.unify(lhs_var, rhs_var, env); + + // If unification failed, short-circuit + if (!unify_result.isOk()) { + try self.unifyWith(expr_var, .err, env); + return does_fx; + } + + // Set root expr. If unifications succeeded this will the the + // num, otherwise the propgate error + _ = try self.unify(expr_var, lhs_var, env); + } + }, + .sub, .mul, .div, .rem, .div_trunc => { + // For now, we'll constrain both operands to be numbers + // In the future, this will use static dispatch based on the lhs type + + // We check the lhs and the rhs independently, then unify them with + // each other. This ensures that all errors are surfaced and the + // operands are the same type + switch (expected) { + .expected => |expectation| { + const lhs_instantiated = try self.instantiateVar(expectation.var_, env, .{ .explicit = expr_region }); + const rhs_instantiated = try self.instantiateVar(expectation.var_, env, .{ .explicit = expr_region }); + + if (expectation.from_annotation) { + _ = try self.unifyWithCtx(lhs_instantiated, lhs_var, env, .anno); + _ = try self.unifyWithCtx(rhs_instantiated, rhs_var, env, .anno); + } else { + _ = try self.unify(lhs_instantiated, lhs_var, env); + _ = try self.unify(rhs_instantiated, rhs_var, env); + } + }, + .no_expectation => { + // No expectation - operand types will be inferred + // The unification of lhs and rhs below will ensure they're the same type + }, + } + + // Unify left and right together + _ = try self.unify(lhs_var, rhs_var, env); + + // Set root expr. If unifications succeeded this will the the + // num, otherwise the propgate error + _ = try self.unify(expr_var, lhs_var, env); + }, + .lt, .gt, .le, .ge => { + // Ensure the operands are the same type + const result = try self.unify(lhs_var, rhs_var, env); + + if (result.isOk()) { + const fresh_bool = try self.freshBool(env, expr_region); + _ = try self.unify(expr_var, fresh_bool, env); + } else { + try self.unifyWith(expr_var, .err, env); + } + }, + .eq => { + // `a == b` desugars to `a.is_eq(b)` with additional constraint that a and b have the same type + // Constraint: a.is_eq : a, b -> ret_type (ret_type is NOT hardcoded to Bool) + + // Unify lhs and rhs to ensure both operands have the same type + const unify_result = try self.unify(lhs_var, rhs_var, env); + + // If unification failed, short-circuit and set the expression to error + if (!unify_result.isOk()) { + try self.unifyWith(expr_var, .err, env); + return does_fx; + } + + // Create the function type: lhs_type, rhs_type -> ret_type (fresh flex var) + const args_range = try self.types.appendVars(&.{ lhs_var, rhs_var }); + const is_eq_ret_var = try self.fresh(env, expr_region); + + const constraint_fn_var = try self.freshFromContent(.{ .structure = .{ .fn_unbound = Func{ + .args = args_range, + .ret = is_eq_ret_var, + .needs_instantiation = false, + } } }, env, expr_region); + + // Create the is_eq constraint + const is_eq_constraint = StaticDispatchConstraint{ + .fn_name = self.cir.idents.is_eq, + .fn_var = constraint_fn_var, + .origin = .desugared_binop, + }; + const constraint_range = try self.types.appendStaticDispatchConstraints(&.{is_eq_constraint}); + + // Create a constrained flex and unify it with the lhs (receiver) + const constrained_var = try self.freshFromContent( + .{ .flex = Flex{ .name = null, .constraints = constraint_range } }, + env, + expr_region, + ); + + _ = try self.unify(constrained_var, lhs_var, env); + + // The expression type is whatever is_eq returns + _ = try self.unify(expr_var, is_eq_ret_var, env); + }, + .ne => { + // `a != b` desugars to `a.is_eq(b).not()` with additional constraint that a and b have the same type + // Constraint 1: a.is_eq : a, b -> is_eq_ret + // Constraint 2: is_eq_ret.not : is_eq_ret -> final_ret + + // Unify lhs and rhs to ensure both operands have the same type + const unify_result = try self.unify(lhs_var, rhs_var, env); + + // If unification failed, short-circuit and set the expression to error + if (!unify_result.isOk()) { + try self.unifyWith(expr_var, .err, env); + return does_fx; + } + + // Create fresh var for the final return type (result of not) + const not_ret_var = try self.fresh(env, expr_region); + + // Create is_eq_ret_var as a constrained flex WITH the not constraint + // We need to create the not constraint first, but it references is_eq_ret_var... + // Solution: create a plain flex first for the not fn arg, then create the + // constrained is_eq_ret_var and use it in the is_eq function type + + // Create a placeholder for is_eq_ret that we'll use in the not constraint + const is_eq_ret_placeholder = try self.fresh(env, expr_region); + + // Create the not constraint referencing the placeholder + const not_args_range = try self.types.appendVars(&.{is_eq_ret_placeholder}); + const not_fn_var = try self.freshFromContent(.{ .structure = .{ .fn_unbound = Func{ + .args = not_args_range, + .ret = not_ret_var, + .needs_instantiation = false, + } } }, env, expr_region); + + const not_constraint = StaticDispatchConstraint{ + .fn_name = self.cir.idents.not, + .fn_var = not_fn_var, + .origin = .desugared_binop, + }; + + // Create is_eq_ret_var WITH the not constraint attached + const not_constraint_range = try self.types.appendStaticDispatchConstraints(&.{not_constraint}); + const is_eq_ret_var = try self.freshFromContent( + .{ .flex = Flex{ .name = null, .constraints = not_constraint_range } }, + env, + expr_region, + ); + + // Unify placeholder with the real constrained var so they're the same + _ = try self.unify(is_eq_ret_placeholder, is_eq_ret_var, env); + + // Constraint 1: is_eq method on lhs type (returns the constrained is_eq_ret_var) + const is_eq_args_range = try self.types.appendVars(&.{ lhs_var, rhs_var }); + const is_eq_fn_var = try self.freshFromContent(.{ .structure = .{ .fn_unbound = Func{ + .args = is_eq_args_range, + .ret = is_eq_ret_var, + .needs_instantiation = false, + } } }, env, expr_region); + + const is_eq_constraint = StaticDispatchConstraint{ + .fn_name = self.cir.idents.is_eq, + .fn_var = is_eq_fn_var, + .origin = .desugared_binop, + }; + + // Add is_eq constraint to lhs + const is_eq_constraint_range = try self.types.appendStaticDispatchConstraints(&.{is_eq_constraint}); + const lhs_constrained_var = try self.freshFromContent( + .{ .flex = Flex{ .name = null, .constraints = is_eq_constraint_range } }, + env, + expr_region, + ); + _ = try self.unify(lhs_constrained_var, lhs_var, env); + + // The expression type is the return type of not + _ = try self.unify(expr_var, not_ret_var, env); + }, + .@"and" => { + const lhs_fresh_bool = try self.freshBool(env, expr_region); + const lhs_result = try self.unify(lhs_fresh_bool, lhs_var, env); + self.setDetailIfTypeMismatch(lhs_result, .{ .invalid_bool_binop = .{ + .binop_expr = expr_idx, + .problem_side = .lhs, + .binop = .@"and", + } }); + + const rhs_fresh_bool = try self.freshBool(env, expr_region); + const rhs_result = try self.unify(rhs_fresh_bool, rhs_var, env); + self.setDetailIfTypeMismatch(rhs_result, .{ .invalid_bool_binop = .{ + .binop_expr = expr_idx, + .problem_side = .rhs, + .binop = .@"and", + } }); + + // Unify left and right together + _ = try self.unify(lhs_var, rhs_var, env); + + // Set root expr. If unifications succeeded this will the the + // num, otherwise the propgate error + _ = try self.unify(expr_var, lhs_var, env); + }, + .@"or" => { + const lhs_fresh_bool = try self.freshBool(env, expr_region); + const lhs_result = try self.unify(lhs_fresh_bool, lhs_var, env); + self.setDetailIfTypeMismatch(lhs_result, .{ .invalid_bool_binop = .{ + .binop_expr = expr_idx, + .problem_side = .lhs, + .binop = .@"and", + } }); + + const rhs_fresh_bool = try self.freshBool(env, expr_region); + const rhs_result = try self.unify(rhs_fresh_bool, rhs_var, env); + self.setDetailIfTypeMismatch(rhs_result, .{ .invalid_bool_binop = .{ + .binop_expr = expr_idx, + .problem_side = .rhs, + .binop = .@"and", + } }); + + // Unify left and right together + _ = try self.unify(lhs_var, rhs_var, env); + + // Set root expr. If unifications succeeded this will the the + // num, otherwise the propagate error + _ = try self.unify(expr_var, lhs_var, env); + }, + } + return does_fx; } @@ -2303,530 +4991,936 @@ fn setProblemTypeMismatchDetail(self: *Self, problem_idx: problem.Problem.Idx, m } } -// tests // - -// test "minimum signed values fit in their respective types" { -// const test_cases = .{ -// .{ .value = -128, .type = types_mod.Num.Int.Precision.i8, .should_fit = true }, -// .{ .value = -129, .type = types_mod.Num.Int.Precision.i8, .should_fit = false }, -// .{ .value = -32768, .type = types_mod.Num.Int.Precision.i16, .should_fit = true }, -// .{ .value = -32769, .type = types_mod.Num.Int.Precision.i16, .should_fit = false }, -// .{ .value = -2147483648, .type = types_mod.Num.Int.Precision.i32, .should_fit = true }, -// .{ .value = -2147483649, .type = types_mod.Num.Int.Precision.i32, .should_fit = false }, -// .{ .value = -9223372036854775808, .type = types_mod.Num.Int.Precision.i64, .should_fit = true }, -// .{ .value = -9223372036854775809, .type = types_mod.Num.Int.Precision.i64, .should_fit = false }, -// .{ .value = -170141183460469231731687303715884105728, .type = types_mod.Num.Int.Precision.i128, .should_fit = true }, -// }; - -// const gpa = std.testing.allocator; - -// var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); -// try module_env.initModuleEnvFields(gpa, "Test"); -// defer module_env.deinit(); - -// var problems = try ProblemStore.initCapacity(gpa, 16); -// defer problems.deinit(gpa); - -// var snapshots = try SnapshotStore.initCapacity(gpa, 16); -// defer snapshots.deinit(); - -// var unify_scratch = try unifier.Scratch.init(gpa); -// defer unify_scratch.deinit(); - -// var occurs_scratch = try occurs.Scratch.init(gpa); -// defer occurs_scratch.deinit(); - -// inline for (test_cases) |tc| { -// // Calculate the magnitude -// const u128_val: u128 = if (tc.value < 0) @as(u128, @intCast(-(tc.value + 1))) + 1 else @as(u128, @intCast(tc.value)); - -// // Apply the branchless adjustment for minimum signed values -// const is_negative = @as(u1, @intFromBool(tc.value < 0)); -// const is_power_of_2 = @as(u1, @intFromBool(u128_val != 0 and (u128_val & (u128_val - 1)) == 0)); -// const is_minimum_signed = is_negative & is_power_of_2; -// const adjusted_val = u128_val - is_minimum_signed; - -// // Create requirements based on adjusted value -// const requirements = types_mod.Num.IntRequirements{ -// .sign_needed = tc.value < 0, -// .bits_needed = @intFromEnum(types_mod.Num.Int.BitsNeeded.fromValue(adjusted_val)), -// }; - -// const literal_var = try module_env.types.freshFromContent(types_mod.Content{ .structure = .{ .num = .{ .num_unbound = requirements } } }); -// const type_var = try module_env.types.freshFromContent(types_mod.Content{ .structure = .{ .num = .{ .num_compact = .{ .int = tc.type } } } }); - -// const result = try unifier.unify( -// &module_env, -// &module_env.types, -// &problems, -// &snapshots, -// &unify_scratch, -// &occurs_scratch, -// literal_var, -// type_var, -// ); - -// if (tc.should_fit) { -// try std.testing.expect(result == .ok); -// } else { -// try std.testing.expect(result == .problem); -// } -// } -// } - -// test "minimum signed values have correct bits_needed" { -// const test_cases = .{ -// .{ .value = -128, .expected_bits = types_mod.Num.Int.BitsNeeded.@"7" }, -// .{ .value = -129, .expected_bits = types_mod.Num.Int.BitsNeeded.@"8" }, -// .{ .value = -32768, .expected_bits = types_mod.Num.Int.BitsNeeded.@"9_to_15" }, -// .{ .value = -32769, .expected_bits = types_mod.Num.Int.BitsNeeded.@"16" }, -// .{ .value = -2147483648, .expected_bits = types_mod.Num.Int.BitsNeeded.@"17_to_31" }, -// .{ .value = -2147483649, .expected_bits = types_mod.Num.Int.BitsNeeded.@"32" }, -// .{ .value = -9223372036854775808, .expected_bits = types_mod.Num.Int.BitsNeeded.@"33_to_63" }, -// .{ .value = -9223372036854775809, .expected_bits = types_mod.Num.Int.BitsNeeded.@"64" }, -// .{ .value = -170141183460469231731687303715884105728, .expected_bits = types_mod.Num.Int.BitsNeeded.@"65_to_127" }, -// }; - -// inline for (test_cases) |tc| { -// // Calculate the magnitude -// const u128_val: u128 = if (tc.value < 0) @as(u128, @intCast(-(tc.value + 1))) + 1 else @as(u128, @intCast(tc.value)); - -// // Apply the branchless adjustment for minimum signed values -// const is_negative = @as(u1, if (tc.value < 0) 1 else 0); -// const is_power_of_2 = @as(u1, if (u128_val != 0 and (u128_val & (u128_val - 1)) == 0) 1 else 0); -// const is_minimum_signed = is_negative & is_power_of_2; -// const adjusted_val = u128_val - is_minimum_signed; - -// const bits_needed = types_mod.Num.Int.BitsNeeded.fromValue(adjusted_val); -// try std.testing.expectEqual(tc.expected_bits, bits_needed); -// } -// } - -// test "branchless minimum signed value detection" { -// const test_cases = .{ -// // Minimum signed values (negative powers of 2) -// .{ .value = -1, .is_minimum = true }, // magnitude 1 = 2^0 -// .{ .value = -2, .is_minimum = true }, // magnitude 2 = 2^1 -// .{ .value = -4, .is_minimum = true }, // magnitude 4 = 2^2 -// .{ .value = -8, .is_minimum = true }, // magnitude 8 = 2^3 -// .{ .value = -16, .is_minimum = true }, // magnitude 16 = 2^4 -// .{ .value = -32, .is_minimum = true }, // magnitude 32 = 2^5 -// .{ .value = -64, .is_minimum = true }, // magnitude 64 = 2^6 -// .{ .value = -128, .is_minimum = true }, // magnitude 128 = 2^7 -// .{ .value = -256, .is_minimum = true }, // magnitude 256 = 2^8 -// .{ .value = -32768, .is_minimum = true }, // magnitude 32768 = 2^15 -// .{ .value = -2147483648, .is_minimum = true }, // magnitude 2^31 - -// // Not minimum signed values -// .{ .value = 128, .is_minimum = false }, // positive -// .{ .value = -3, .is_minimum = false }, // magnitude 3 (not power of 2) -// .{ .value = -5, .is_minimum = false }, // magnitude 5 (not power of 2) -// .{ .value = -127, .is_minimum = false }, // magnitude 127 (not power of 2) -// .{ .value = -129, .is_minimum = false }, // magnitude 129 (not power of 2) -// .{ .value = -130, .is_minimum = false }, // magnitude 130 (not power of 2) -// .{ .value = 0, .is_minimum = false }, // zero -// }; - -// inline for (test_cases) |tc| { -// const value: i128 = tc.value; -// const u128_val: u128 = if (value < 0) @as(u128, @intCast(-(value + 1))) + 1 else @as(u128, @intCast(value)); - -// const is_negative = @as(u1, @intFromBool(value < 0)); -// const is_power_of_2 = @as(u1, @intFromBool(u128_val != 0 and (u128_val & (u128_val - 1)) == 0)); -// const is_minimum_signed = is_negative & is_power_of_2; - -// const expected: u1 = @intFromBool(tc.is_minimum); -// try std.testing.expectEqual(expected, is_minimum_signed); -// } -// } - -// test "verify -128 produces 7 bits needed" { -// const value: i128 = -128; -// const u128_val: u128 = if (value < 0) @as(u128, @intCast(-(value + 1))) + 1 else @as(u128, @intCast(value)); - -// // Check intermediate values -// try std.testing.expectEqual(@as(u128, 128), u128_val); - -// const is_negative = @as(u1, @intFromBool(value < 0)); -// const is_power_of_2 = @as(u1, @intFromBool(u128_val != 0 and (u128_val & (u128_val - 1)) == 0)); -// const is_minimum_signed = is_negative & is_power_of_2; - -// try std.testing.expectEqual(@as(u1, 1), is_negative); -// try std.testing.expectEqual(@as(u1, 1), is_power_of_2); -// try std.testing.expectEqual(@as(u1, 1), is_minimum_signed); - -// const adjusted_val = u128_val - is_minimum_signed; -// try std.testing.expectEqual(@as(u128, 127), adjusted_val); - -// // Test that 127 maps to 7 bits -// const bits_needed = types_mod.Num.Int.BitsNeeded.fromValue(adjusted_val); -// try std.testing.expectEqual(types_mod.Num.Int.BitsNeeded.@"7", bits_needed); -// try std.testing.expectEqual(@as(u8, 7), bits_needed.toBits()); -// } - -// test "lambda with record field access infers correct type" { -// // The lambda |x, y| { x: x, y: y }.x should have type a, b -> a -// // And when annotated as I32, I32 -> I32, it should unify correctly. -// // This is a regression test against a bug that previously existed in that scenario. -// const gpa = std.testing.allocator; - -// // Create a minimal environment for testing -// var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); -// defer module_env.deinit(); - -// try module_env.initModuleEnvFields(gpa, "Test"); -// const cir = &module_env; - -// const empty_modules: []const *ModuleEnv = &.{}; -// var solver = try Self.init(gpa, &module_env.types, cir, empty_modules, &cir.store.regions); -// defer solver.deinit(); - -// // Create type variables for the lambda parameters -// const param_x_var = try module_env.types.fresh(); -// const param_y_var = try module_env.types.fresh(); - -// // Create a record with fields x and y -// var record_fields = std.ArrayList(types_mod.RecordField).init(gpa); -// defer record_fields.deinit(); - -// const x_ident = try module_env.idents.insert(gpa, base.Ident.for_text("x")); -// const y_ident = try module_env.idents.insert(gpa, base.Ident.for_text("y")); - -// try record_fields.append(.{ .name = x_ident, .var_ = param_x_var }); -// try record_fields.append(.{ .name = y_ident, .var_ = param_y_var }); - -// const fields_range = try module_env.types.appendRecordFields(record_fields.items); -// const ext_var = try module_env.types.fresh(); -// const record_content = types_mod.Content{ -// .structure = .{ -// .record = .{ -// .fields = fields_range, -// .ext = ext_var, -// }, -// }, -// }; -// _ = try module_env.types.freshFromContent(record_content); - -// // Simulate field access: record.x -// // The result type should unify with param_x_var -// const field_access_var = try module_env.types.fresh(); -// _ = try solver.unify(field_access_var, param_x_var); - -// // Create the lambda type: param_x, param_y -> field_access_result -// const lambda_content = try module_env.types.mkFuncUnbound(&[_]types_mod.Var{ param_x_var, param_y_var }, field_access_var); -// const lambda_var = try module_env.types.freshFromContent(lambda_content); - -// // The lambda should have type a, b -> a (param_x and return type are unified) -// const resolved_lambda = module_env.types.resolveVar(lambda_var); -// try std.testing.expect(resolved_lambda.desc.content == .structure); -// try std.testing.expect(resolved_lambda.desc.content.structure == .fn_unbound); - -// const func = resolved_lambda.desc.content.structure.fn_unbound; -// const args = module_env.types.sliceVars(func.args); -// try std.testing.expectEqual(@as(usize, 2), args.len); - -// // Verify that first parameter and return type resolve to the same variable -// const first_param_resolved = module_env.types.resolveVar(args[0]); -// const return_resolved = module_env.types.resolveVar(func.ret); -// try std.testing.expectEqual(first_param_resolved.var_, return_resolved.var_); - -// // Now test with annotation: I32, I32 -> I32 -// const i32_content = types_mod.Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }; -// const i32_var1 = try module_env.types.freshFromContent(i32_content); -// const i32_var2 = try module_env.types.freshFromContent(i32_content); -// const i32_var3 = try module_env.types.freshFromContent(i32_content); - -// const annotated_func = try module_env.types.mkFuncPure(&[_]types_mod.Var{ i32_var1, i32_var2 }, i32_var3); -// const annotation_var = try module_env.types.freshFromContent(annotated_func); - -// // Unify the lambda with its annotation -// const unify_result = try solver.unify(lambda_var, annotation_var); -// try std.testing.expect(unify_result == .ok); - -// // Verify the lambda now has the concrete type -// const final_resolved = module_env.types.resolveVar(lambda_var); -// try std.testing.expect(final_resolved.desc.content == .structure); -// try std.testing.expect(final_resolved.desc.content.structure == .fn_pure); - -// // Test call site: when calling with integer literals -// const num_unbound = types_mod.Content{ .structure = .{ .num = .{ .num_unbound = .{ .sign_needed = false, .bits_needed = 0 } } } }; -// const lit1_var = try module_env.types.freshFromContent(num_unbound); -// const lit2_var = try module_env.types.freshFromContent(num_unbound); -// const call_result_var = try module_env.types.fresh(); - -// const expected_func_content = try module_env.types.mkFuncUnbound(&[_]types_mod.Var{ lit1_var, lit2_var }, call_result_var); -// const expected_func_var = try module_env.types.freshFromContent(expected_func_content); - -// // The critical fix: unify expected with actual (not the other way around) -// const call_unify_result = try solver.unify(expected_func_var, lambda_var); -// try std.testing.expect(call_unify_result == .ok); - -// // Verify the literals got constrained to I32 -// const lit1_resolved = module_env.types.resolveVar(lit1_var); -// try std.testing.expect(lit1_resolved.desc.content == .structure); -// try std.testing.expect(lit1_resolved.desc.content.structure == .num); -// try std.testing.expect(lit1_resolved.desc.content.structure.num == .int_precision); -// try std.testing.expect(lit1_resolved.desc.content.structure.num.int_precision == .i32); -// } - -// test "dot access properly unifies field types with parameters" { -// // This test verifies that e_dot_access correctly handles field access -// // and unifies the field type with the expression result type. - -// const gpa = std.testing.allocator; - -// // Create a minimal environment for testing -// var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); -// defer module_env.deinit(); - -// try module_env.initModuleEnvFields(gpa, "Test"); -// const cir = &module_env; - -// const empty_modules: []const *ModuleEnv = &.{}; -// var solver = try Self.init(gpa, &module_env.types, cir, empty_modules, &cir.store.regions); -// defer solver.deinit(); - -// // Create a parameter type variable -// const param_var = try module_env.types.fresh(); - -// // Create a record with field "x" of the same type as the parameter -// var record_fields = std.ArrayList(types_mod.RecordField).init(gpa); -// defer record_fields.deinit(); - -// const x_ident = try module_env.idents.insert(gpa, base.Ident.for_text("x")); -// try record_fields.append(.{ .name = x_ident, .var_ = param_var }); - -// const fields_range = try module_env.types.appendRecordFields(record_fields.items); -// const ext_var = try module_env.types.fresh(); -// const record_content = types_mod.Content{ -// .structure = .{ -// .record = .{ -// .fields = fields_range, -// .ext = ext_var, -// }, -// }, -// }; -// const record_var = try module_env.types.freshFromContent(record_content); - -// // Create a dot access result variable -// const dot_access_var = try module_env.types.fresh(); - -// // Simulate the dot access logic from checkExpr -// const resolved_record = module_env.types.resolveVar(record_var); -// try std.testing.expect(resolved_record.desc.content == .structure); -// try std.testing.expect(resolved_record.desc.content.structure == .record); - -// const record = resolved_record.desc.content.structure.record; -// const fields = module_env.types.getRecordFieldsSlice(record.fields); - -// // Find field "x" and unify with dot access result -// var found_field = false; -// for (fields.items(.name), fields.items(.var_)) |field_name, field_var| { -// if (field_name == x_ident) { -// _ = try solver.unify(dot_access_var, field_var); -// found_field = true; -// break; -// } -// } -// try std.testing.expect(found_field); - -// // Verify that dot_access_var and param_var now resolve to the same variable -// const dot_resolved = module_env.types.resolveVar(dot_access_var); -// const param_resolved = module_env.types.resolveVar(param_var); -// try std.testing.expectEqual(dot_resolved.var_, param_resolved.var_); - -// // Test with unbound record -// const unbound_record_content = types_mod.Content{ -// .structure = .{ -// .record_unbound = fields_range, -// }, -// }; -// const unbound_record_var = try module_env.types.freshFromContent(unbound_record_content); -// const dot_access_var2 = try module_env.types.fresh(); - -// // Same test with record_unbound -// const resolved_unbound = module_env.types.resolveVar(unbound_record_var); -// try std.testing.expect(resolved_unbound.desc.content == .structure); -// try std.testing.expect(resolved_unbound.desc.content.structure == .record_unbound); - -// const unbound_record = resolved_unbound.desc.content.structure.record_unbound; -// const unbound_fields = module_env.types.getRecordFieldsSlice(unbound_record); - -// found_field = false; -// for (unbound_fields.items(.name), unbound_fields.items(.var_)) |field_name, field_var| { -// if (field_name == x_ident) { -// _ = try solver.unify(dot_access_var2, field_var); -// found_field = true; -// break; -// } -// } -// try std.testing.expect(found_field); - -// // Verify unification worked -// const dot2_resolved = module_env.types.resolveVar(dot_access_var2); -// try std.testing.expectEqual(dot2_resolved.var_, param_resolved.var_); -// } - -// test "call site unification order matters for concrete vs flexible types" { -// // This test verifies that unification order matters when dealing with -// // concrete types (like I32) and flexible types (like Num(*)). -// // -// // At call sites, we must unify in the correct order: -// // - unify(flexible, concrete) ✓ succeeds - flexible types can be constrained -// // - unify(concrete, flexible) ✗ fails - concrete types cannot become more general -// // -// // The test demonstrates the complete type checking scenario: -// // 1. Unification order matters (concrete→flexible fails, flexible→concrete succeeds) -// // 2. When unifying function types in the correct order, the flexible argument -// // types are properly constrained to match the concrete parameter types -// // 3. Numeric arguments start as flexible num_unbound types -// // 4. After unification, they become concrete I32 types -// const gpa = std.testing.allocator; - -// var common_env = try CommonEnv.init(gpa, try gpa.dupe(u8, "")); -// // Module env takes ownership of Common env -- no need to deinit here - -// // Create a minimal environment for testing -// var module_env = try ModuleEnv.init(gpa, &common_env); -// defer module_env.deinit(); - -// try module_env.initModuleEnvFields(gpa, "Test"); -// const cir = &module_env; - -// const empty_modules: []const *ModuleEnv = &.{}; -// var solver = try Self.init(gpa, &module_env.types, cir, empty_modules, &cir.store.regions); -// defer solver.deinit(); - -// // First, verify basic number unification works as expected -// const i32_content = types_mod.Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }; -// const i32_test = try module_env.types.freshFromContent(i32_content); -// const num_unbound = types_mod.Content{ .structure = .{ .num = .{ .num_unbound = .{ .sign_needed = false, .bits_needed = 0 } } } }; -// const flex_test = try module_env.types.freshFromContent(num_unbound); - -// // Flexible number should unify with concrete I32 and become I32 -// const basic_result = try solver.unify(flex_test, i32_test); -// try std.testing.expect(basic_result == .ok); - -// // Verify the flexible variable was constrained to I32 -// const flex_resolved = module_env.types.resolveVar(flex_test); -// switch (flex_resolved.desc.content) { -// .structure => |s| switch (s) { -// .num => |n| switch (n) { -// .int_precision => |prec| { -// try std.testing.expectEqual(types_mod.Num.Int.Precision.i32, prec); -// }, -// else => return error.TestUnexpectedResult, -// }, -// else => return error.TestUnexpectedResult, -// }, -// else => return error.TestUnexpectedResult, -// } - -// // Now test with function types -// // Create a concrete function type: I32, I32 -> I32 -// const i32_var1 = try module_env.types.freshFromContent(i32_content); -// const i32_var2 = try module_env.types.freshFromContent(i32_content); -// const i32_var3 = try module_env.types.freshFromContent(i32_content); - -// const concrete_func = try module_env.types.mkFuncPure(&[_]types_mod.Var{ i32_var1, i32_var2 }, i32_var3); -// const concrete_func_var = try module_env.types.freshFromContent(concrete_func); - -// // Create flexible argument types (like integer literals) -// const arg1_var = try module_env.types.freshFromContent(num_unbound); -// const arg2_var = try module_env.types.freshFromContent(num_unbound); -// const result_var = try module_env.types.fresh(); - -// // Create expected function type from arguments -// const expected_func = try module_env.types.mkFuncUnbound(&[_]types_mod.Var{ arg1_var, arg2_var }, result_var); -// const expected_func_var = try module_env.types.freshFromContent(expected_func); - -// // The wrong order: unify(concrete, expected) would fail -// // because I32 can't unify with Num(*) -// const wrong_order_result = try solver.unify(concrete_func_var, expected_func_var); -// try std.testing.expect(wrong_order_result == .problem); - -// // After failed unification, both variables become error types -// const concrete_after_fail = module_env.types.resolveVar(concrete_func_var); -// const expected_after_fail = module_env.types.resolveVar(expected_func_var); -// try std.testing.expectEqual(types_mod.Content.err, concrete_after_fail.desc.content); -// try std.testing.expectEqual(types_mod.Content.err, expected_after_fail.desc.content); - -// // Now simulate a complete type checking scenario for a function call -// // This is what happens when type checking code like: myFunc(1, 2) -// // where myFunc : I32, I32 -> I32 - -// // Step 1: Create the known function type (I32, I32 -> I32) -// const i32_var4 = try module_env.types.freshFromContent(i32_content); -// const i32_var5 = try module_env.types.freshFromContent(i32_content); -// const i32_var6 = try module_env.types.freshFromContent(i32_content); -// const known_func = try module_env.types.mkFuncPure(&[_]types_mod.Var{ i32_var4, i32_var5 }, i32_var6); -// const known_func_var = try module_env.types.freshFromContent(known_func); - -// // Step 2: Create flexible argument types (representing literals like 1 and 2) -// const call_arg1 = try module_env.types.freshFromContent(num_unbound); -// const call_arg2 = try module_env.types.freshFromContent(num_unbound); - -// // Verify the arguments start as flexible num_unbound types -// const arg1_before = module_env.types.resolveVar(call_arg1); -// const arg2_before = module_env.types.resolveVar(call_arg2); - -// switch (arg1_before.desc.content) { -// .structure => |s| switch (s) { -// .num => |n| switch (n) { -// .num_unbound => {}, // Expected -// else => return error.TestUnexpectedResult, -// }, -// else => return error.TestUnexpectedResult, -// }, -// else => return error.TestUnexpectedResult, -// } - -// switch (arg2_before.desc.content) { -// .structure => |s| switch (s) { -// .num => |n| switch (n) { -// .num_unbound => {}, // Expected -// else => return error.TestUnexpectedResult, -// }, -// else => return error.TestUnexpectedResult, -// }, -// else => return error.TestUnexpectedResult, -// } - -// // Step 3: Create the expected function type from the call site -// // This represents the type we expect based on the arguments -// const call_result = try module_env.types.fresh(); -// const call_func = try module_env.types.mkFuncUnbound(&[_]types_mod.Var{ call_arg1, call_arg2 }, call_result); -// const call_func_var = try module_env.types.freshFromContent(call_func); - -// // Step 4: Unify the expected type with the known type -// // This is the key step - unify(expected, known) in the correct order -// const unify_result = try solver.unify(call_func_var, known_func_var); -// try std.testing.expect(unify_result == .ok); - -// // Step 5: Verify that the call arguments were constrained to I32 -// // This simulates what happens in real type checking - the argument -// // variables used at the call site get constrained by the function type - -// // Step 6: Verify that both arguments are now constrained to I32 -// for ([_]types_mod.Var{ call_arg1, call_arg2 }) |arg| { -// const arg_resolved = module_env.types.resolveVar(arg); -// switch (arg_resolved.desc.content) { -// .structure => |s| switch (s) { -// .num => |n| switch (n) { -// .int_precision => |prec| { -// try std.testing.expectEqual(types_mod.Num.Int.Precision.i32, prec); -// }, -// .num_compact => |compact| switch (compact) { -// .int => |prec| { -// try std.testing.expectEqual(types_mod.Num.Int.Precision.i32, prec); -// }, -// else => return error.TestUnexpectedResult, -// }, -// else => return error.TestUnexpectedResult, -// }, -// else => return error.TestUnexpectedResult, -// }, -// else => return error.TestUnexpectedResult, -// } -// } -// } +// copy type from other module // + +// external type lookups // + +const ExternalType = struct { + local_var: Var, + other_cir_node_idx: CIR.Node.Idx, + other_cir: *const ModuleEnv, +}; + +/// Copy a variable from a different module into this module's types store. +/// +/// IMPORTANT: The caller must instantiate this variable before unifing +/// against it. This avoid poisoning the copied variable in the types store if +/// unification fails. +fn resolveVarFromExternal( + self: *Self, + import_idx: CIR.Import.Idx, + node_idx: u16, +) std.mem.Allocator.Error!?ExternalType { + // First try to use the resolved module index from the imports store + // This is the proper way to map import indices to module positions + const module_idx = self.cir.imports.getResolvedModule(import_idx) orelse blk: { + // Fallback: if not resolved, use the import index directly + // This maintains backwards compatibility with tests that don't call resolveImports + break :blk @intFromEnum(import_idx); + }; + if (module_idx < self.imported_modules.len) { + const other_module_cir = self.imported_modules[module_idx]; + const other_module_env = other_module_cir; + + // The idx of the expression in the other module + const target_node_idx = @as(CIR.Node.Idx, @enumFromInt(node_idx)); + + // Check if we've already copied this import + const cache_key = ImportCacheKey{ + .module_idx = import_idx, + .node_idx = target_node_idx, + }; + + const copied_var = if (self.import_cache.get(cache_key)) |cached_var| + // Reuse the previously copied type. + cached_var + else blk: { + // First time importing this type - copy it and cache the result + const imported_var = @as(Var, @enumFromInt(@intFromEnum(target_node_idx))); + + // Every node should have a corresponding type entry + std.debug.assert(@intFromEnum(imported_var) < other_module_env.types.len()); + + const new_copy = try self.copyVar(imported_var, other_module_env, null); + try self.import_cache.put(self.gpa, cache_key, new_copy); + break :blk new_copy; + }; + + return .{ + .local_var = copied_var, + .other_cir_node_idx = target_node_idx, + .other_cir = other_module_env, + }; + } else { + return null; + } +} + +/// Instantiate a variable, writing su +fn copyVar(self: *Self, other_module_var: Var, other_module_env: *const ModuleEnv, mb_region: ?Region) std.mem.Allocator.Error!Var { + // First, reset state + self.var_map.clearRetainingCapacity(); + + // Copy the var from the dest type store into this type store + const copied_var = try copy_import.copyVar( + &other_module_env.*.types, + self.types, + other_module_var, + &self.var_map, + other_module_env.getIdentStoreConst(), + self.cir.getIdentStore(), + self.gpa, + ); + + const region = if (mb_region) |region| region else base.Region.zero(); + + // If we had to insert any new type variables, ensure that we have + // corresponding regions for them. This is essential for error reporting. + if (self.var_map.count() > 0) { + var iterator = self.var_map.iterator(); + while (iterator.next()) |x| { + // Get the newly created var + const fresh_var = x.value_ptr.*; + try self.fillInRegionsThrough(fresh_var); + + self.setRegionAt(fresh_var, region); + } + } + + // Assert that we have regions for every type variable + self.debugAssertArraysInSync(); + + return copied_var; +} + +// nominal type checking helpers // + +/// Result of checking a nominal type usage +const NominalCheckResult = enum { + /// Successfully checked the nominal type + ok, + /// An error occurred (already reported and target_var set to error) + err, +}; + +/// Check a nominal type usage (either in pattern or expression context). +/// This is the shared logic for `.nominal`, `.nominal_external`, `.e_nominal`, and `.e_nominal_external`. +/// +/// Parameters: +/// - target_var: The type variable to unify with (pattern_var or expr_var) +/// - actual_backing_var: The type variable of the backing expression/pattern +/// - nominal_type_decl_var: The type variable from the nominal type declaration +/// - backing_type: The kind of backing type (tag, record, tuple, value) +/// - region: The source region for instantiation +/// - env: The type checking environment +fn checkNominalTypeUsage( + self: *Self, + target_var: Var, + actual_backing_var: Var, + nominal_type_decl_var: Var, + backing_type: CIR.Expr.NominalBackingType, + region: Region, + env: *Env, +) std.mem.Allocator.Error!NominalCheckResult { + // Instantiate the nominal type declaration + const nominal_var = try self.instantiateVar(nominal_type_decl_var, env, .{ .explicit = region }); + const nominal_resolved = self.types.resolveVar(nominal_var).desc.content; + + if (nominal_resolved == .structure and nominal_resolved.structure == .nominal_type) { + const nominal_type = nominal_resolved.structure.nominal_type; + + // If this nominal type is opaque and we're not in the defining module + // then report an error + if (!nominal_type.canLiftInner(self.cir.module_name_idx)) { + _ = try self.problems.appendProblem(self.cir.gpa, .{ .cannot_access_opaque_nominal = .{ + .var_ = target_var, + .nominal_type_name = nominal_type.ident.ident_idx, + } }); + + // Mark the entire expression as having a type error + try self.unifyWith(target_var, .err, env); + return .err; + } + + // Extract the backing type variable from the nominal type + // E.g. ConList(a) := [Cons(a, ConstList), Nil] + // ^^^^^^^^^^^^^^^^^^^^^^^^^ + const nominal_backing_var = self.types.getNominalBackingVar(nominal_type); + + // Unify what the user wrote with the backing type of the nominal + // E.g. ConList.Cons(...) <-> [Cons(a, ConsList(a)), Nil] + // ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ + const result = try self.unify(nominal_backing_var, actual_backing_var, env); + + // Handle the result of unification + switch (result) { + .ok => { + // If unification succeeded, this is a valid instance of the nominal type + // So we set the target's type to be the nominal type + _ = try self.unify(target_var, nominal_var, env); + return .ok; + }, + .problem => |problem_idx| { + // Unification failed - the constructor is incompatible with the nominal type + // Set a specific error message based on the backing type kind + switch (backing_type) { + .tag => { + // Constructor doesn't exist or has wrong arity/types + self.setProblemTypeMismatchDetail(problem_idx, .invalid_nominal_tag); + }, + .record => { + // Record fields don't match the nominal type's backing record + self.setProblemTypeMismatchDetail(problem_idx, .invalid_nominal_record); + }, + .tuple => { + // Tuple elements don't match the nominal type's backing tuple + self.setProblemTypeMismatchDetail(problem_idx, .invalid_nominal_tuple); + }, + .value => { + // Value doesn't match the nominal type's backing type + self.setProblemTypeMismatchDetail(problem_idx, .invalid_nominal_value); + }, + } + + // Mark the entire expression as having a type error + try self.unifyWith(target_var, .err, env); + return .err; + }, + } + } else { + // If the nominal type resolves to something other than a nominal_type structure, + // report the error and set the expression to error type + _ = try self.problems.appendProblem(self.cir.gpa, .{ .nominal_type_resolution_failed = .{ + .var_ = target_var, + .nominal_type_decl_var = nominal_type_decl_var, + } }); + try self.unifyWith(target_var, .err, env); + return .err; + } +} + +// validate static dispatch constraints // + +/// Handle a recursive static dispatch constraint by creating a RecursionVar +/// +/// When we detect that a constraint check would recurse (the variable is already +/// being checked in the call stack), we create a RecursionVar to represent the +/// recursive structure and prevent infinite loops. +/// +/// The RecursionVar points back to the original variable structure, allowing +/// equirecursive unification to properly handle the cycle. +fn handleRecursiveConstraint( + self: *Self, + var_: types_mod.Var, + depth: usize, + env: *Env, +) std.mem.Allocator.Error!void { + // Create the RecursionVar content that points to the original structure + const rec_var_content = types_mod.Content{ + .recursion_var = .{ + .structure = var_, + .name = null, // Could be enhanced to carry debug name + }, + }; + + // Create a new type variable to represent the recursion point + // Use the current environment's rank for the recursion var + const recursion_var = try self.freshFromContent(rec_var_content, env, self.getRegionAt(var_)); + + // Create RecursionInfo to track the recursion metadata + _ = types_mod.RecursionInfo{ + .recursion_var = recursion_var, + .depth = depth, + }; + + // Store the recursion info in the deferred constraint + // Note: This will be enhanced in later implementation to properly + // update the constraint with the recursion info +} + +/// Check static dispatch constraints +/// +/// Note that new constraints can be added as we are processing. For example: +/// +/// Test := [Val(Str)].{ +/// to_str = |Test.Val(s)| s +/// to_str2 = |test| test.to_str() +/// } +/// main = Test.Val("hello").to_str2() +/// +/// Initially, we only have to check constraint for `Test.to_str2`. But when we +/// process that, we then have to check `Test.to_str`. +fn checkDeferredStaticDispatchConstraints(self: *Self, env: *Env) std.mem.Allocator.Error!void { + var deferred_constraint_len = env.deferred_static_dispatch_constraints.items.items.len; + var deferred_constraint_index: usize = 0; + while (deferred_constraint_index < deferred_constraint_len) : ({ + deferred_constraint_index += 1; + deferred_constraint_len = env.deferred_static_dispatch_constraints.items.items.len; + }) { + const deferred_constraint = env.deferred_static_dispatch_constraints.items.items[deferred_constraint_index]; + const dispatcher_resolved = self.types.resolveVar(deferred_constraint.var_); + const dispatcher_content = dispatcher_resolved.desc.content; + + // Detect recursive constraints + // Check if this var is already in the constraint check stack + for (self.constraint_check_stack.items, 0..) |stack_var, depth| { + if (stack_var == dispatcher_resolved.var_) { + // Found recursion! Create a RecursionVar to handle this properly + try self.handleRecursiveConstraint(dispatcher_resolved.var_, depth, env); + continue; + } + } + + // Not recursive - push to stack and proceed normally + try self.constraint_check_stack.append(self.gpa, dispatcher_resolved.var_); + defer _ = self.constraint_check_stack.pop(); + + if (dispatcher_content == .err) { + // If the root type is an error, then skip constraint checking + const constraints = self.types.sliceStaticDispatchConstraints(deferred_constraint.constraints); + for (constraints) |constraint| { + try self.markConstraintFunctionAsError(constraint, env); + } + try self.unifyWith(deferred_constraint.var_, .err, env); + } else if (dispatcher_content == .rigid) { + // Get the rigid variable and the constraints it has defined + const rigid = dispatcher_content.rigid; + const rigid_constraints = self.types.sliceStaticDispatchConstraints(rigid.constraints); + + // Get the deferred constraints to validate against + const deferred_constraints = self.types.sliceStaticDispatchConstraints(deferred_constraint.constraints); + + // First, special case if this rigid has no constraints + if (deferred_constraints.len > 0 and rigid_constraints.len == 0) { + const constraint = deferred_constraints[0]; + try self.reportConstraintError( + deferred_constraint.var_, + constraint, + .{ .missing_method = .rigid }, + env, + ); + continue; + } + + // Build a map of constraints the rigid has + self.ident_to_var_map.clearRetainingCapacity(); + try self.ident_to_var_map.ensureUnusedCapacity(@intCast(rigid_constraints.len)); + for (rigid_constraints) |rigid_constraint| { + self.ident_to_var_map.putAssumeCapacity(rigid_constraint.fn_name, rigid_constraint.fn_var); + } + + // Iterate over the constraints + for (deferred_constraints) |constraint| { + // Extract the function and return type from the constraint + const resolved_constraint = self.types.resolveVar(constraint.fn_var); + const mb_resolved_func = resolved_constraint.desc.content.unwrapFunc(); + std.debug.assert(mb_resolved_func != null); + const resolved_func = mb_resolved_func.?; + + // Then, lookup the inferred constraint in the actual list of rigid constraints + if (self.ident_to_var_map.get(constraint.fn_name)) |rigid_var| { + // Unify the actual function var against the inferred var + // + // TODO: For better error messages, we should check if these + // types are functions, unify each arg, etc. This should look + // similar to e_call + const result = try self.unify(rigid_var, constraint.fn_var, env); + if (result.isProblem()) { + try self.unifyWith(deferred_constraint.var_, .err, env); + try self.unifyWith(resolved_func.ret, .err, env); + } + } else { + try self.reportConstraintError( + deferred_constraint.var_, + constraint, + .{ .missing_method = .nominal }, + env, + ); + continue; + } + } + } else if (dispatcher_content == .flex) { + // If the root type is aa flex, then we there's nothing to check + continue; + } else if (dispatcher_content == .structure and dispatcher_content.structure == .nominal_type) { + // If the root type is a nominal type, then this is valid static dispatch + const nominal_type = dispatcher_content.structure.nominal_type; + + // Get the module ident that this type was defined in + const original_module_ident = nominal_type.origin_module; + + // Check if the nominal type in question is defined in this module + const is_this_module = original_module_ident == self.builtin_ctx.module_name; + + // Get the list of exposed items to check + const original_env: *const ModuleEnv = blk: { + if (is_this_module) { + break :blk self.cir; + } else if (original_module_ident == self.cir.idents.builtin_module) { + // For builtin types, use the builtin module environment directly + if (self.builtin_ctx.builtin_module) |builtin_env| { + break :blk builtin_env; + } else { + // This happens when compiling the Builtin module itself + break :blk self.cir; + } + } else { + // Look up the module in auto_imported_types. + // Note: auto_imported_types maps TYPE names to their modules, but + // here we're using the origin_module (a MODULE name). This works + // because imported types have entries keyed by their type name. + std.debug.assert(self.auto_imported_types != null); + const auto_imported_types = self.auto_imported_types.?; + + const mb_original_module_env = auto_imported_types.get(original_module_ident); + std.debug.assert(mb_original_module_env != null); + break :blk mb_original_module_env.?.env; + } + }; + + // Get some data about the nominal type + const region = self.getRegionAt(deferred_constraint.var_); + + // Iterate over the constraints + const constraints = self.types.sliceStaticDispatchConstraints(deferred_constraint.constraints); + for (constraints) |constraint| { + // Extract the function and return type from the constraint + const resolved_constraint = self.types.resolveVar(constraint.fn_var); + const mb_resolved_func = resolved_constraint.desc.content.unwrapFunc(); + std.debug.assert(mb_resolved_func != null); + const resolved_func = mb_resolved_func.?; + + // Look up the method in the original env using index-based lookup. + // Methods are stored with qualified names like "Type.method" (or "Module.Type.method" for builtins). + const method_ident = original_env.lookupMethodIdentFromEnvConst(self.cir, nominal_type.ident.ident_idx, constraint.fn_name) orelse { + // Method name doesn't exist in target module + try self.reportConstraintError( + deferred_constraint.var_, + constraint, + .{ .missing_method = .nominal }, + env, + ); + continue; + }; + + // Get the def index in the original env + const node_idx_in_original_env = original_env.getExposedNodeIndexById(method_ident) orelse { + // The ident exists but isn't exposed as a def + try self.reportConstraintError( + deferred_constraint.var_, + constraint, + .{ .missing_method = .nominal }, + env, + ); + continue; + }; + + const def_idx: CIR.Def.Idx = @enumFromInt(@as(u32, @intCast(node_idx_in_original_env))); + const def_var: Var = ModuleEnv.varFrom(def_idx); + + if (is_this_module) { + // Check if we've processed this def already. + const def = original_env.store.getDef(def_idx); + const mb_processing_def = self.top_level_ptrns.get(def.pattern); + if (mb_processing_def) |processing_def| { + std.debug.assert(processing_def.def_idx == def_idx); + switch (processing_def.status) { + .not_processed => { + var sub_env = try self.env_pool.acquire(.generalized); + defer self.env_pool.release(sub_env); + + try self.checkDef(def_idx, &sub_env); + }, + .processing => { + // Recursive reference during static dispatch resolution. + // The def is still being processed, so we'll use its + // current (non-generalized) type. + }, + .processed => {}, + } + } + } + + // Copy the actual method from the dest module env to this module env + const real_method_var = if (is_this_module) blk: { + break :blk try self.instantiateVar(def_var, env, .{ .explicit = region }); + } else blk: { + // Copy the method from the other module's type store + const copied_var = try self.copyVar(def_var, original_env, region); + // For builtin methods, we need to instantiate the copied var to convert + // rigid type variables to flex, so they can unify with the call site + const is_builtin = original_module_ident == self.cir.idents.builtin_module; + if (is_builtin) { + break :blk try self.instantiateVar(copied_var, env, .{ .explicit = region }); + } else { + break :blk copied_var; + } + }; + + // Unify the actual function var against the inferred var + // We break this down into arg-by-arg and return type unification + // for better error messages (instead of showing the whole function types) + + // Extract the function type from the real method + const resolved_real = self.types.resolveVar(real_method_var); + const mb_real_func = resolved_real.desc.content.unwrapFunc(); + if (mb_real_func == null) { + // The looked-up definition is not a function - report as missing method + try self.reportConstraintError( + deferred_constraint.var_, + constraint, + .{ .missing_method = .nominal }, + env, + ); + continue; + } + const real_func = mb_real_func.?; + + // Check arity matches + const constraint_args = self.types.sliceVars(resolved_func.args); + const real_args = self.types.sliceVars(real_func.args); + + if (constraint_args.len != real_args.len) { + // Arity mismatch - the method exists but has wrong number of arguments + try self.reportConstraintError( + deferred_constraint.var_, + constraint, + .{ .missing_method = .nominal }, + env, + ); + continue; + } + + // Unify each argument pair + var any_arg_failed = false; + for (constraint_args, real_args) |constraint_arg, real_arg| { + const arg_result = try self.unify(real_arg, constraint_arg, env); + if (arg_result.isProblem()) { + any_arg_failed = true; + } + } + + // Unify return types - this will generate the error with the expression region + const ret_result = try self.unify(real_func.ret, resolved_func.ret, env); + + if (any_arg_failed or ret_result.isProblem()) { + try self.unifyWith(deferred_constraint.var_, .err, env); + try self.unifyWith(resolved_func.ret, .err, env); + } + // Note: from_numeral constraint validation happens during comptime evaluation + // in ComptimeEvaluator.validateDeferredNumericLiterals() + } + } else if (dispatcher_content == .structure and + (dispatcher_content.structure == .record or + dispatcher_content.structure == .tuple or + dispatcher_content.structure == .tag_union or + dispatcher_content.structure == .empty_record or + dispatcher_content.structure == .empty_tag_union)) + { + // Anonymous structural types (records, tuples, tag unions) have implicit is_eq + // only if all their components also support is_eq + const constraints = self.types.sliceStaticDispatchConstraints(deferred_constraint.constraints); + for (constraints) |constraint| { + // Check if this is a call to is_eq (anonymous types have implicit structural equality) + if (constraint.fn_name == self.cir.idents.is_eq) { + // Check if all components of this anonymous type support is_eq + if (self.typeSupportsIsEq(dispatcher_content.structure)) { + // All components support is_eq, unify return type with Bool + const resolved_constraint = self.types.resolveVar(constraint.fn_var); + const mb_resolved_func = resolved_constraint.desc.content.unwrapFunc(); + if (mb_resolved_func) |resolved_func| { + const region = self.getRegionAt(deferred_constraint.var_); + const bool_var = try self.freshBool(env, region); + _ = try self.unify(bool_var, resolved_func.ret, env); + } + } else { + // Some component doesn't support is_eq (e.g., contains a function) + try self.reportEqualityError( + deferred_constraint.var_, + constraint, + env, + ); + } + } else { + // Other methods are not supported on anonymous types + try self.reportConstraintError( + deferred_constraint.var_, + constraint, + .not_nominal, + env, + ); + } + } + } else { + // If the root type is anything but a nominal type or anonymous structural type, push an error + // This handles function types, which do not support any methods + + const constraints = self.types.sliceStaticDispatchConstraints(deferred_constraint.constraints); + if (constraints.len > 0) { + // Report errors for ALL failing constraints, not just the first one + for (constraints) |constraint| { + // For is_eq constraints, use the specific equality error message + // Use ident index comparison instead of string comparison + if (constraint.fn_name == self.cir.idents.is_eq) { + try self.reportEqualityError( + deferred_constraint.var_, + constraint, + env, + ); + } else { + try self.reportConstraintError( + deferred_constraint.var_, + constraint, + .not_nominal, + env, + ); + } + } + } else { + // Deferred constraint checks should always have at least one constraint. + // If we hit this, there's a compiler bug in how constraints are tracked. + std.debug.assert(false); + } + } + } + + // Now that we've processed all constraints, reset the array + env.deferred_static_dispatch_constraints.items.clearRetainingCapacity(); +} + +/// Check if a structural type supports is_eq. +/// A type supports is_eq if: +/// - It's not a function type +/// - All of its components (record fields, tuple elements, tag payloads) also support is_eq +/// - For nominal types, check if their backing type supports is_eq +fn typeSupportsIsEq(self: *Self, flat_type: types_mod.FlatType) bool { + return switch (flat_type) { + // Function types do not support is_eq + .fn_pure, .fn_effectful, .fn_unbound => false, + + // Empty types trivially support is_eq + .empty_record, .empty_tag_union => true, + + // Records support is_eq if all field types support is_eq + .record => |record| { + const fields_slice = self.types.getRecordFieldsSlice(record.fields); + for (fields_slice.items(.var_)) |field_var| { + if (!self.varSupportsIsEq(field_var)) return false; + } + return true; + }, + + // Tuples support is_eq if all element types support is_eq + .tuple => |tuple| { + const elems = self.types.sliceVars(tuple.elems); + for (elems) |elem_var| { + if (!self.varSupportsIsEq(elem_var)) return false; + } + return true; + }, + + // Tag unions support is_eq if all payload types support is_eq + .tag_union => |tag_union| { + const tags_slice = self.types.getTagsSlice(tag_union.tags); + for (tags_slice.items(.args)) |tag_args| { + const args = self.types.sliceVars(tag_args); + for (args) |arg_var| { + if (!self.varSupportsIsEq(arg_var)) return false; + } + } + return true; + }, + + // Nominal types support is_eq if their backing type supports is_eq + .nominal_type => |nominal| { + const backing_var = self.types.getNominalBackingVar(nominal); + return self.varSupportsIsEq(backing_var); + }, + + // Unbound records: resolve and check the resolved type + .record_unbound => |fields| { + // Check each field in the unbound record + const fields_slice = self.types.getRecordFieldsSlice(fields); + for (fields_slice.items(.var_)) |field_var| { + if (!self.varSupportsIsEq(field_var)) return false; + } + return true; + }, + }; +} + +/// Check if a type variable supports is_eq by resolving it and checking its content +fn varSupportsIsEq(self: *Self, var_: Var) bool { + const resolved = self.types.resolveVar(var_); + return switch (resolved.desc.content) { + .structure => |s| self.typeSupportsIsEq(s), + // Flex/rigid vars: we optimistically assume they support is_eq. + // This is sound because if the variable is later unified with a type + // that doesn't support is_eq (like a function), unification will fail. + .flex, .rigid => true, + // Aliases: check the underlying type + .alias => |alias| self.varSupportsIsEq(self.types.getAliasBackingVar(alias)), + // Recursion vars: we must assume they support is_eq to avoid infinite loops. + // Recursive types like List support is_eq if their element type does. + .recursion_var => true, + // Error types: allow them to proceed + .err => true, + }; +} + +/// Mark a constraint function's return type as error +fn markConstraintFunctionAsError(self: *Self, constraint: StaticDispatchConstraint, env: *Env) !void { + const resolved_constraint = self.types.resolveVar(constraint.fn_var); + const mb_resolved_func = resolved_constraint.desc.content.unwrapFunc(); + std.debug.assert(mb_resolved_func != null); + const resolved_func = mb_resolved_func.?; + try self.unifyWith(resolved_func.ret, .err, env); +} + +/// Report a constraint validation error +fn reportConstraintError( + self: *Self, + dispatcher_var: Var, + constraint: StaticDispatchConstraint, + kind: union(enum) { + missing_method: problem.DispatcherDoesNotImplMethod.DispatcherType, + not_nominal, + }, + env: *Env, +) !void { + const snapshot = try self.snapshots.snapshotVarForError(self.types, &self.type_writer, dispatcher_var); + const constraint_problem = switch (kind) { + .missing_method => |dispatcher_type| problem.Problem{ .static_dispach = .{ + .dispatcher_does_not_impl_method = .{ + .dispatcher_var = dispatcher_var, + .dispatcher_snapshot = snapshot, + .dispatcher_type = dispatcher_type, + .fn_var = constraint.fn_var, + .method_name = constraint.fn_name, + .origin = constraint.origin, + }, + } }, + .not_nominal => problem.Problem{ .static_dispach = .{ + .dispatcher_not_nominal = .{ + .dispatcher_var = dispatcher_var, + .dispatcher_snapshot = snapshot, + .fn_var = constraint.fn_var, + .method_name = constraint.fn_name, + }, + } }, + }; + _ = try self.problems.appendProblem(self.cir.gpa, constraint_problem); + + try self.markConstraintFunctionAsError(constraint, env); +} + +/// Report an error when an anonymous type doesn't support equality +fn reportEqualityError( + self: *Self, + dispatcher_var: Var, + constraint: StaticDispatchConstraint, + env: *Env, +) !void { + const snapshot = try self.snapshots.snapshotVarForError(self.types, &self.type_writer, dispatcher_var); + const equality_problem = problem.Problem{ .static_dispach = .{ + .type_does_not_support_equality = .{ + .dispatcher_var = dispatcher_var, + .dispatcher_snapshot = snapshot, + .fn_var = constraint.fn_var, + }, + } }; + _ = try self.problems.appendProblem(self.cir.gpa, equality_problem); + + try self.markConstraintFunctionAsError(constraint, env); +} + +/// Pool for reusing Env instances to avoid repeated allocations +const EnvPool = struct { + available: std.ArrayList(Env), + gpa: Allocator, + + fn init(gpa: Allocator) std.mem.Allocator.Error!EnvPool { + var pool = try std.ArrayList(Env).initCapacity(gpa, 8); + for (0..8) |_| { + pool.appendAssumeCapacity(try Env.init(gpa, .generalized)); + } + return .{ + .available = pool, + .gpa = gpa, + }; + } + + fn deinit(self: *EnvPool) void { + for (self.available.items) |*env| { + env.deinit(self.gpa); + } + self.available.deinit(self.gpa); + } + + /// Acquire an Env from the pool, or create a new one if none available + fn acquire(self: *EnvPool, at: Rank) std.mem.Allocator.Error!Env { + const trace = tracy.trace(@src()); + defer trace.end(); + + if (self.available.pop()) |env| { + // Reset the env for reuse + var reused_env = env; + reused_env.reset(); + return reused_env; + } else { + // Otherwise init a new one and ensure there's room to put it back + // into the pool when we're done using it + try self.available.ensureUnusedCapacity(self.gpa, 1); + return try Env.init(self.gpa, at); + } + } + + /// Return an Env to the pool for reuse. + /// If the pool cannot fit it, then deinit the env + /// + /// * If we acquire an existing env from the pool, there should be a slot + /// available to return it + /// * If we init a new env when we acquire, the acquire func should expand the + /// pool so we have room to return it + fn release(self: *EnvPool, env: Env) void { + const trace = tracy.trace(@src()); + defer trace.end(); + + var releasable_env = env; + releasable_env.reset(); + self.available.append(self.gpa, releasable_env) catch { + // If we can't add to the pool, just deinit this env + releasable_env.deinit(self.gpa); + }; + } +}; + +/// Create the import mapping for type display names in error messages. +/// +/// This builds a mapping from fully-qualified type identifiers to their shortest display names +/// based on what's in scope. The mapping is built from: +/// 1. Auto-imported builtin types (e.g., Bool, Str, Dec, U64, etc.) +/// 2. User import statements with their aliases +/// +/// When multiple imports could refer to the same type, the shortest name wins. +/// This ensures error messages show types the way users would write them in their code. +pub fn createImportMapping( + gpa: std.mem.Allocator, + idents: *Ident.Store, + cir: *const ModuleEnv, + builtin_module: ?*const ModuleEnv, + builtin_indices: ?CIR.BuiltinIndices, + auto_imported_types: ?*const std.AutoHashMap(Ident.Idx, can.Can.AutoImportedType), +) std.mem.Allocator.Error!types_mod.import_mapping.ImportMapping { + var mapping = types_mod.import_mapping.ImportMapping.init(gpa); + errdefer mapping.deinit(); + + // Step 1: Add auto-imported builtin types + if (builtin_module) |builtin_env| { + if (builtin_indices) |indices| { + const fields = @typeInfo(CIR.BuiltinIndices).@"struct".fields; + inline for (fields) |field| { + // Only process Statement.Idx fields (skip Ident.Idx fields) + if (field.type == CIR.Statement.Idx) { + const stmt_idx: CIR.Statement.Idx = @field(indices, field.name); + + // Skip invalid statement indices (index 0 is typically invalid/sentinel) + const stmt_idx_int = @intFromEnum(stmt_idx); + if (stmt_idx_int != 0) { + const stmt = builtin_env.store.getStatement(stmt_idx); + switch (stmt) { + .s_nominal_decl => |decl| { + const header = builtin_env.store.getTypeHeader(decl.header); + const qualified_name = builtin_env.getIdentText(header.name); + const relative_name = builtin_env.getIdentText(header.relative_name); + + // Extract display name (last component after dots) + const display_name = blk: { + var last_dot: usize = 0; + for (qualified_name, 0..) |c, i| { + if (c == '.') last_dot = i + 1; + } + break :blk qualified_name[last_dot..]; + }; + + const qualified_ident = try idents.insert(gpa, Ident.for_text(qualified_name)); + const relative_ident = try idents.insert(gpa, Ident.for_text(relative_name)); + const display_ident = try idents.insert(gpa, Ident.for_text(display_name)); + + // Add mapping for qualified_name -> display_name + if (mapping.get(qualified_ident)) |existing_ident| { + const existing_name = idents.getText(existing_ident); + if (displayNameIsBetter(display_name, existing_name)) { + try mapping.put(qualified_ident, display_ident); + } + } else { + try mapping.put(qualified_ident, display_ident); + } + + // Also add mapping for relative_name -> display_name + // This ensures types stored with relative_name (like "Num.Numeral") also map to display_name + if (mapping.get(relative_ident)) |existing_ident| { + const existing_name = idents.getText(existing_ident); + if (displayNameIsBetter(display_name, existing_name)) { + try mapping.put(relative_ident, display_ident); + } + } else { + try mapping.put(relative_ident, display_ident); + } + }, + else => { + // Skip non-nominal statements (e.g., nested types that aren't directly importable) + }, + } + } + } + } + } + } + + // Step 2: Copy user import mappings from the ModuleEnv + // These were built during canonicalization when processing import statements + var iter = cir.import_mapping.iterator(); + while (iter.next()) |entry| { + const qualified_ident = entry.key_ptr.*; + const local_ident = entry.value_ptr.*; + + // Get the text for comparison + const local_name = cir.getIdentText(local_ident); + + if (mapping.get(qualified_ident)) |existing_display| { + // Only replace if the new name is "better" + const existing_name = idents.getText(existing_display); + if (displayNameIsBetter(local_name, existing_name)) { + try mapping.put(qualified_ident, local_ident); + } + } else { + try mapping.put(qualified_ident, local_ident); + } + } + + _ = auto_imported_types; // Not needed anymore - mapping is built during canonicalization + + return mapping; +} + +/// Determine if `new_name` is a "better" display name than `existing_name`. +/// Returns true if new_name should replace existing_name. +/// +/// The rules are: +/// 1. Shorter names are better (fewer characters to read in error messages) +/// 2. For equal lengths, lexicographically smaller wins (deterministic regardless of import order) +pub fn displayNameIsBetter(new_name: []const u8, existing_name: []const u8) bool { + // Shorter is better + if (new_name.len != existing_name.len) { + return new_name.len < existing_name.len; + } + // Equal length: lexicographic comparison (lower byte value wins) + for (new_name, existing_name) |new_byte, existing_byte| { + if (new_byte != existing_byte) { + return new_byte < existing_byte; + } + } + // Identical strings - no replacement needed + return false; +} diff --git a/src/check/copy_import.zig b/src/check/copy_import.zig index 30e1687563..97fc048cac 100644 --- a/src/check/copy_import.zig +++ b/src/check/copy_import.zig @@ -11,6 +11,9 @@ const types_mod = @import("types"); const TypesStore = types_mod.Store; const Var = types_mod.Var; +const Flex = types_mod.Flex; +const Rigid = types_mod.Rigid; +const StaticDispatchConstraint = types_mod.StaticDispatchConstraint; const Content = types_mod.Content; const FlatType = types_mod.FlatType; const Alias = types_mod.Alias; @@ -38,24 +41,28 @@ pub fn copyVar( dest_idents: *base.Ident.Store, allocator: std.mem.Allocator, ) std.mem.Allocator.Error!Var { + const resolved = source_store.resolveVar(source_var); + // Check if we've already copied this variable - if (var_mapping.get(source_var)) |dest_var| { + if (var_mapping.get(resolved.var_)) |dest_var| { return dest_var; } - const resolved = source_store.resolveVar(source_var); - // Create a placeholder variable first to break cycles const placeholder_var = try dest_store.fresh(); // Record the mapping immediately to handle recursive types - try var_mapping.put(source_var, placeholder_var); + try var_mapping.put(resolved.var_, placeholder_var); // Now copy the content (which may recursively reference this variable) const dest_content = try copyContent(source_store, dest_store, resolved.desc.content, var_mapping, source_idents, dest_idents, allocator); // Update the placeholder with the actual content - try dest_store.setVarContent(placeholder_var, dest_content); + try dest_store.dangerousSetVarDesc(placeholder_var, .{ + .content = dest_content, + .rank = types_mod.Rank.generalized, + .mark = types_mod.Mark.none, + }); return placeholder_var; } @@ -70,14 +77,86 @@ fn copyContent( allocator: std.mem.Allocator, ) std.mem.Allocator.Error!Content { return switch (content) { - .flex_var => |maybe_ident| Content{ .flex_var = maybe_ident }, - .rigid_var => |ident| Content{ .rigid_var = ident }, + .flex => |flex| Content{ .flex = try copyFlex(source_store, dest_store, flex, var_mapping, source_idents, dest_idents, allocator) }, + .rigid => |rigid| Content{ .rigid = try copyRigid(source_store, dest_store, rigid, var_mapping, source_idents, dest_idents, allocator) }, .alias => |alias| Content{ .alias = try copyAlias(source_store, dest_store, alias, var_mapping, source_idents, dest_idents, allocator) }, .structure => |flat_type| Content{ .structure = try copyFlatType(source_store, dest_store, flat_type, var_mapping, source_idents, dest_idents, allocator) }, + .recursion_var => |rec_var| blk: { + // Copy the recursion var by copying the structure it points to + const copied_structure = try copyVar(source_store, dest_store, rec_var.structure, var_mapping, source_idents, dest_idents, allocator); + break :blk Content{ .recursion_var = .{ .structure = copied_structure, .name = rec_var.name } }; + }, .err => Content.err, }; } +fn copyFlex( + source_store: *const TypesStore, + dest_store: *TypesStore, + source_flex: Flex, + var_mapping: *VarMapping, + source_idents: *const base.Ident.Store, + dest_idents: *base.Ident.Store, + allocator: std.mem.Allocator, +) std.mem.Allocator.Error!Flex { + // Translate the type name ident + const mb_translated_name = blk: { + if (source_flex.name) |name_ident| { + const name_bytes = source_idents.getText(name_ident); + const translated_name = try dest_idents.insert(allocator, base.Ident.for_text(name_bytes)); + break :blk translated_name; + } else { + break :blk null; + } + }; + + // Copy the constraints + const dest_constraints_range = try copyStaticDispatchConstraints( + source_store, + dest_store, + source_flex.constraints, + var_mapping, + source_idents, + dest_idents, + allocator, + ); + + return Flex{ + .name = mb_translated_name, + .constraints = dest_constraints_range, + }; +} + +fn copyRigid( + source_store: *const TypesStore, + dest_store: *TypesStore, + source_rigid: Rigid, + var_mapping: *VarMapping, + source_idents: *const base.Ident.Store, + dest_idents: *base.Ident.Store, + allocator: std.mem.Allocator, +) std.mem.Allocator.Error!Rigid { + // Translate the type name ident + const name_bytes = source_idents.getText(source_rigid.name); + const translated_name = try dest_idents.insert(allocator, base.Ident.for_text(name_bytes)); + + // Copy the constraints + const dest_constraints_range = try copyStaticDispatchConstraints( + source_store, + dest_store, + source_rigid.constraints, + var_mapping, + source_idents, + dest_idents, + allocator, + ); + + return Rigid{ + .name = translated_name, + .constraints = dest_constraints_range, + }; +} + fn copyAlias( source_store: *const TypesStore, dest_store: *TypesStore, @@ -87,22 +166,21 @@ fn copyAlias( dest_idents: *base.Ident.Store, allocator: std.mem.Allocator, ) std.mem.Allocator.Error!Alias { - // Translate the type name ident const type_name_str = source_idents.getText(source_alias.ident.ident_idx); const translated_ident = try dest_idents.insert(allocator, base.Ident.for_text(type_name_str)); - var dest_args = std.ArrayList(Var).init(dest_store.gpa); - defer dest_args.deinit(); + var dest_args = std.ArrayList(Var).empty; + defer dest_args.deinit(dest_store.gpa); const origin_backing = source_store.getAliasBackingVar(source_alias); const dest_backing = try copyVar(source_store, dest_store, origin_backing, var_mapping, source_idents, dest_idents, allocator); - try dest_args.append(dest_backing); + try dest_args.append(dest_store.gpa, dest_backing); const origin_args = source_store.sliceAliasArgs(source_alias); for (origin_args) |arg_var| { const dest_arg = try copyVar(source_store, dest_store, arg_var, var_mapping, source_idents, dest_idents, allocator); - try dest_args.append(dest_arg); + try dest_args.append(dest_store.gpa, dest_arg); } const dest_vars_span = try dest_store.appendVars(dest_args.items); @@ -123,12 +201,7 @@ fn copyFlatType( allocator: std.mem.Allocator, ) std.mem.Allocator.Error!FlatType { return switch (flat_type) { - .str => FlatType.str, - .box => |box_var| FlatType{ .box = try copyVar(source_store, dest_store, box_var, var_mapping, source_idents, dest_idents, allocator) }, - .list => |list_var| FlatType{ .list = try copyVar(source_store, dest_store, list_var, var_mapping, source_idents, dest_idents, allocator) }, - .list_unbound => FlatType.list_unbound, .tuple => |tuple| FlatType{ .tuple = try copyTuple(source_store, dest_store, tuple, var_mapping, source_idents, dest_idents, allocator) }, - .num => |num| FlatType{ .num = try copyNum(source_store, dest_store, num, var_mapping, source_idents, dest_idents, allocator) }, .nominal_type => |nominal| FlatType{ .nominal_type = try copyNominalType(source_store, dest_store, nominal, var_mapping, source_idents, dest_idents, allocator) }, .fn_pure => |func| FlatType{ .fn_pure = try copyFunc(source_store, dest_store, func, var_mapping, source_idents, dest_idents, allocator) }, .fn_effectful => |func| FlatType{ .fn_effectful = try copyFunc(source_store, dest_store, func, var_mapping, source_idents, dest_idents, allocator) }, @@ -136,11 +209,6 @@ fn copyFlatType( .record => |record| FlatType{ .record = try copyRecord(source_store, dest_store, record, var_mapping, source_idents, dest_idents, allocator) }, .tag_union => |tag_union| FlatType{ .tag_union = try copyTagUnion(source_store, dest_store, tag_union, var_mapping, source_idents, dest_idents, allocator) }, .record_unbound => |fields| FlatType{ .record_unbound = try copyRecordFields(source_store, dest_store, fields, var_mapping, source_idents, dest_idents, allocator) }, - .record_poly => |poly| blk: { - const dest_record = try copyRecord(source_store, dest_store, poly.record, var_mapping, source_idents, dest_idents, allocator); - const dest_var = try copyVar(source_store, dest_store, poly.var_, var_mapping, source_idents, dest_idents, allocator); - break :blk FlatType{ .record_poly = .{ .record = dest_record, .var_ = dest_var } }; - }, .empty_record => FlatType.empty_record, .empty_tag_union => FlatType.empty_tag_union, }; @@ -157,40 +225,17 @@ fn copyTuple( ) std.mem.Allocator.Error!types_mod.Tuple { const elems_slice = source_store.sliceVars(tuple.elems); - var dest_elems = std.ArrayList(Var).init(dest_store.gpa); - defer dest_elems.deinit(); + var dest_elems = std.ArrayList(Var).empty; + defer dest_elems.deinit(dest_store.gpa); for (elems_slice) |elem_var| { const dest_elem = try copyVar(source_store, dest_store, elem_var, var_mapping, source_idents, dest_idents, allocator); - try dest_elems.append(dest_elem); + try dest_elems.append(dest_store.gpa, dest_elem); } const dest_range = try dest_store.appendVars(dest_elems.items); return types_mod.Tuple{ .elems = dest_range }; } - -fn copyNum( - source_store: *const TypesStore, - dest_store: *TypesStore, - num: Num, - var_mapping: *VarMapping, - source_idents: *const base.Ident.Store, - dest_idents: *base.Ident.Store, - allocator: std.mem.Allocator, -) std.mem.Allocator.Error!Num { - return switch (num) { - .num_poly => |poly| Num{ .num_poly = .{ .var_ = try copyVar(source_store, dest_store, poly.var_, var_mapping, source_idents, dest_idents, allocator), .requirements = poly.requirements } }, - .int_poly => |poly| Num{ .int_poly = .{ .var_ = try copyVar(source_store, dest_store, poly.var_, var_mapping, source_idents, dest_idents, allocator), .requirements = poly.requirements } }, - .frac_poly => |poly| Num{ .frac_poly = .{ .var_ = try copyVar(source_store, dest_store, poly.var_, var_mapping, source_idents, dest_idents, allocator), .requirements = poly.requirements } }, - .num_unbound => |unbound| Num{ .num_unbound = unbound }, - .int_unbound => |unbound| Num{ .int_unbound = unbound }, - .frac_unbound => |unbound| Num{ .frac_unbound = unbound }, - .int_precision => |precision| Num{ .int_precision = precision }, - .frac_precision => |precision| Num{ .frac_precision = precision }, - .num_compact => |compact| Num{ .num_compact = compact }, - }; -} - fn copyFunc( source_store: *const TypesStore, dest_store: *TypesStore, @@ -202,12 +247,12 @@ fn copyFunc( ) std.mem.Allocator.Error!Func { const args_slice = source_store.sliceVars(func.args); - var dest_args = std.ArrayList(Var).init(dest_store.gpa); - defer dest_args.deinit(); + var dest_args = std.ArrayList(Var).empty; + defer dest_args.deinit(dest_store.gpa); for (args_slice) |arg_var| { const dest_arg = try copyVar(source_store, dest_store, arg_var, var_mapping, source_idents, dest_idents, allocator); - try dest_args.append(dest_arg); + try dest_args.append(dest_store.gpa, dest_arg); } const dest_ret = try copyVar(source_store, dest_store, func.ret, var_mapping, source_idents, dest_idents, allocator); @@ -231,13 +276,13 @@ fn copyRecordFields( ) std.mem.Allocator.Error!types_mod.RecordField.SafeMultiList.Range { const source_fields = source_store.getRecordFieldsSlice(fields_range); - var fresh_fields = std.ArrayList(RecordField).init(allocator); - defer fresh_fields.deinit(); + var fresh_fields = std.ArrayList(RecordField).empty; + defer fresh_fields.deinit(allocator); for (source_fields.items(.name), source_fields.items(.var_)) |name, var_| { const name_str = source_idents.getText(name); const translated_name = try dest_idents.insert(allocator, base.Ident.for_text(name_str)); - _ = try fresh_fields.append(.{ + _ = try fresh_fields.append(allocator, .{ .name = translated_name, // Field names are local to the record type .var_ = try copyVar(source_store, dest_store, var_, var_mapping, source_idents, dest_idents, allocator), }); @@ -282,18 +327,18 @@ fn copyTagUnion( ) std.mem.Allocator.Error!TagUnion { const tags_slice = source_store.getTagsSlice(tag_union.tags); - var fresh_tags = std.ArrayList(Tag).init(allocator); - defer fresh_tags.deinit(); + var fresh_tags = std.ArrayList(Tag).empty; + defer fresh_tags.deinit(allocator); for (tags_slice.items(.name), tags_slice.items(.args)) |name, args_range| { const args_slice = source_store.sliceVars(args_range); - var dest_args = std.ArrayList(Var).init(dest_store.gpa); - defer dest_args.deinit(); + var dest_args = std.ArrayList(Var).empty; + defer dest_args.deinit(dest_store.gpa); for (args_slice) |arg_var| { const dest_arg = try copyVar(source_store, dest_store, arg_var, var_mapping, source_idents, dest_idents, allocator); - try dest_args.append(dest_arg); + try dest_args.append(dest_store.gpa, dest_arg); } const dest_args_range = try dest_store.appendVars(dest_args.items); @@ -301,7 +346,7 @@ fn copyTagUnion( const name_str = source_idents.getText(name); const translated_name = try dest_idents.insert(allocator, base.Ident.for_text(name_str)); - _ = try fresh_tags.append(.{ + _ = try fresh_tags.append(allocator, .{ .name = translated_name, // Tag names are local to the union type .args = dest_args_range, }); @@ -332,17 +377,17 @@ fn copyNominalType( const origin_str = source_idents.getText(source_nominal.origin_module); const translated_origin = try dest_idents.insert(allocator, base.Ident.for_text(origin_str)); - var dest_args = std.ArrayList(Var).init(dest_store.gpa); - defer dest_args.deinit(); + var dest_args = std.ArrayList(Var).empty; + defer dest_args.deinit(dest_store.gpa); const origin_backing = source_store.getNominalBackingVar(source_nominal); const dest_backing = try copyVar(source_store, dest_store, origin_backing, var_mapping, source_idents, dest_idents, allocator); - try dest_args.append(dest_backing); + try dest_args.append(dest_store.gpa, dest_backing); const origin_args = source_store.sliceNominalArgs(source_nominal); for (origin_args) |arg_var| { const dest_arg = try copyVar(source_store, dest_store, arg_var, var_mapping, source_idents, dest_idents, allocator); - try dest_args.append(dest_arg); + try dest_args.append(dest_store.gpa, dest_arg); } const dest_vars_span = try dest_store.appendVars(dest_args.items); @@ -351,5 +396,41 @@ fn copyNominalType( .ident = types_mod.TypeIdent{ .ident_idx = translated_ident }, .vars = .{ .nonempty = dest_vars_span }, .origin_module = translated_origin, + .is_opaque = source_nominal.is_opaque, }; } + +fn copyStaticDispatchConstraints( + source_store: *const TypesStore, + dest_store: *TypesStore, + source_constraints: StaticDispatchConstraint.SafeList.Range, + var_mapping: *VarMapping, + source_idents: *const base.Ident.Store, + dest_idents: *base.Ident.Store, + allocator: std.mem.Allocator, +) std.mem.Allocator.Error!StaticDispatchConstraint.SafeList.Range { + const source_constraints_len = source_constraints.len(); + if (source_constraints_len == 0) { + return StaticDispatchConstraint.SafeList.Range.empty(); + } else { + // Setup tmp state + var dest_constraints = try std.array_list.Managed(StaticDispatchConstraint).initCapacity(dest_store.gpa, source_constraints_len); + defer dest_constraints.deinit(); + + // Iterate over the constraints + for (source_store.sliceStaticDispatchConstraints(source_constraints)) |source_constraint| { + // Translate the fn name + const fn_name_bytes = source_idents.getText(source_constraint.fn_name); + const translated_fn_name = try dest_idents.insert(allocator, base.Ident.for_text(fn_name_bytes)); + + try dest_constraints.append(StaticDispatchConstraint{ + .fn_name = translated_fn_name, + .fn_var = try copyVar(source_store, dest_store, source_constraint.fn_var, var_mapping, source_idents, dest_idents, allocator), + .origin = source_constraint.origin, + }); + } + + const dest_constraints_range = try dest_store.appendStaticDispatchConstraints(dest_constraints.items); + return dest_constraints_range; + } +} diff --git a/src/check/mod.zig b/src/check/mod.zig index 6ed4d421b1..fd089942e3 100644 --- a/src/check/mod.zig +++ b/src/check/mod.zig @@ -31,10 +31,17 @@ test "check tests" { std.testing.refAllDecls(@import("problem.zig")); std.testing.refAllDecls(@import("snapshot.zig")); std.testing.refAllDecls(@import("unify.zig")); + std.testing.refAllDecls(@import("test/cross_module_test.zig")); + std.testing.refAllDecls(@import("test/type_checking_integration.zig")); std.testing.refAllDecls(@import("test/let_polymorphism_integration_test.zig")); - std.testing.refAllDecls(@import("test/let_polymorphism_test.zig")); - std.testing.refAllDecls(@import("test/literal_size_test.zig")); - std.testing.refAllDecls(@import("test/nominal_type_origin_test.zig")); - std.testing.refAllDecls(@import("test/static_dispatch_test.zig")); + std.testing.refAllDecls(@import("test/num_type_requirements_test.zig")); + std.testing.refAllDecls(@import("test/custom_num_type_test.zig")); + std.testing.refAllDecls(@import("test/builtin_scope_test.zig")); + std.testing.refAllDecls(@import("test/num_type_inference_test.zig")); + std.testing.refAllDecls(@import("test/unify_test.zig")); + std.testing.refAllDecls(@import("test/instantiate_tag_union_test.zig")); + std.testing.refAllDecls(@import("test/where_clause_test.zig")); + std.testing.refAllDecls(@import("test/recursive_alias_test.zig")); + std.testing.refAllDecls(@import("test/generalize_redirect_test.zig")); } diff --git a/src/check/occurs.zig b/src/check/occurs.zig index 6e7e4d0165..2680086f52 100644 --- a/src/check/occurs.zig +++ b/src/check/occurs.zig @@ -54,11 +54,6 @@ pub const Result = enum { /// This function accepts a mutable reference to `Store`, but guarantees that it /// _only_ modifies a variable's `Mark`. Before returning, all visited nodes' /// `Mark`s will be reset to `none`. -/// -/// TODO: See if there's a way to represent this ^ in the type system? If we -/// switch the types_store descriptors to use a multi list (which we should do -/// anyway), maybe we can only pass in only a mutable ref to the backing `Mark`s -/// array? pub fn occurs(types_store: *Store, scratch: *Scratch, var_: Var) std.mem.Allocator.Error!Result { scratch.reset(); @@ -160,21 +155,10 @@ const CheckOccurs = struct { switch (root.desc.content) { .structure => |flat_type| { switch (flat_type) { - .str => {}, - .box => |sub_var| { - try self.occursSubVar(root, sub_var, ctx.allowRecursion()); - }, - .list => |sub_var| { - try self.occursSubVar(root, sub_var, ctx.allowRecursion()); - }, - .list_unbound => { - // list_unbound has no sub-variables to check - }, .tuple => |tuple| { const elems = self.types_store.sliceVars(tuple.elems); try self.occursSubVars(root, elems, ctx); }, - .num => {}, .nominal_type => |nominal_type| { // Check all argument vars using iterator var arg_iter = self.types_store.iterNominalArgs(nominal_type); @@ -209,12 +193,6 @@ const CheckOccurs = struct { const fields_slice = self.types_store.getRecordFieldsSlice(fields); try self.occursSubVars(root, fields_slice.items(.var_), ctx.allowRecursion()); }, - .record_poly => |poly| { - const fields = self.types_store.getRecordFieldsSlice(poly.record.fields); - try self.occursSubVars(root, fields.items(.var_), ctx.allowRecursion()); - try self.occursSubVar(root, poly.record.ext, ctx); - try self.occursSubVar(root, poly.var_, ctx); - }, .tag_union => |tag_union| { const tags = self.types_store.getTagsSlice(tag_union.tags); for (tags.items(.args)) |tag_args| { @@ -236,8 +214,19 @@ const CheckOccurs = struct { const backing_var = self.types_store.getAliasBackingVar(alias); try self.occursSubVar(root, backing_var, ctx); }, - .flex_var => {}, - .rigid_var => {}, + .flex => |flex| { + // Check static dispatch constraints to detect cycles through constraint functions + // For example: a.plus : a, a -> a (where the function type refers back to 'a') + const constraints = self.types_store.static_dispatch_constraints.sliceRange(flex.constraints); + for (constraints) |constraint| { + try self.occursSubVar(root, constraint.fn_var, ctx); + } + }, + .rigid => {}, + .recursion_var => |rec_var| { + // Check the structure the recursion var points to + try self.occursSubVar(root, rec_var.structure, ctx); + }, .err => {}, } self.scratch.popSeen(); @@ -310,9 +299,12 @@ pub const Scratch = struct { visited: MkSafeList(DescStoreIdx), pub fn init(gpa: std.mem.Allocator) std.mem.Allocator.Error!Self { - // TODO: eventually use herusitics here to determine sensible defaults - // Rust compiler inits with 1024 capacity. But that feels like a lot. - // Typical recursion cases should only be a few layers deep? + // Initial capacities are conservative estimates. Lists grow dynamically as needed. + // Rust compiler uses 1024, but that's likely overkill for typical Roc code. + // These values handle common cases: + // - seen/err_chain: 32 - typical type depth is much shallower + // - visited: 64 - covers most type traversals without reallocation + // Future optimization: profile real codebases to tune these values. return .{ .gpa = gpa, .seen = try Var.SafeList.initCapacity(gpa, 32), @@ -381,35 +373,13 @@ test "occurs: no recurcion (v = Str)" { var scratch = try Scratch.init(gpa); defer scratch.deinit(); - const str_var = try types_store.freshFromContent(Content{ .structure = .str }); + const str_var = try types_store.freshFromContent(Content{ .structure = .empty_record }); const result = occurs(&types_store, &scratch, str_var); try std.testing.expectEqual(.not_recursive, result); } -test "occurs: direct recursion (v = List v)" { - const gpa = std.testing.allocator; - var types_store = try Store.init(gpa); - defer types_store.deinit(); - - var scratch = try Scratch.init(gpa); - defer scratch.deinit(); - - const list_var = try types_store.fresh(); - const list_content = Content{ - .structure = .{ .list = list_var }, - }; - try types_store.setRootVarContent(list_var, list_content); - - const result = occurs(&types_store, &scratch, list_var); - try std.testing.expectEqual(.recursive_anonymous, result); - - const err_chain = scratch.errChainSlice(); - try std.testing.expectEqual(1, err_chain.len); - try std.testing.expectEqual(list_var, err_chain[0]); -} - -test "occurs: indirect recursion (v1 = Box v2, v2 = List v1)" { +test "occurs: no recursion through two levels (v1 = Box(v2), v2 = Str)" { const gpa = std.testing.allocator; var types_store = try Store.init(gpa); defer types_store.deinit(); @@ -420,31 +390,16 @@ test "occurs: indirect recursion (v1 = Box v2, v2 = List v1)" { const v1 = try types_store.fresh(); const v2 = try types_store.fresh(); - try types_store.setRootVarContent(v1, Content{ .structure = .{ .box = v2 } }); - try types_store.setRootVarContent(v2, Content{ .structure = .{ .list = v1 } }); - - const result = occurs(&types_store, &scratch, v1); - try std.testing.expectEqual(.recursive_anonymous, result); - - const err_chain = scratch.errChainSlice(); - try std.testing.expectEqual(2, err_chain.len); - try std.testing.expectEqual(v2, err_chain[0]); - try std.testing.expectEqual(v1, err_chain[1]); -} - -test "occurs: no recursion through two levels (v1 = Box v2, v2 = Str)" { - const gpa = std.testing.allocator; - var types_store = try Store.init(gpa); - defer types_store.deinit(); - - var scratch = try Scratch.init(gpa); - defer scratch.deinit(); - - const v1 = try types_store.fresh(); - const v2 = try types_store.fresh(); - - try types_store.setRootVarContent(v1, Content{ .structure = .{ .box = v2 } }); - try types_store.setRootVarContent(v2, Content{ .structure = .str }); + // Create a nominal Box type wrapping v2 + const backing_var = try types_store.freshFromContent(Content{ .structure = .empty_record }); + try types_store.setVarContent(v1, try types_store.mkNominal( + undefined, + backing_var, + &.{v2}, + Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 0 }, + false, + )); + try types_store.setRootVarContent(v2, Content{ .structure = .empty_record }); const result = occurs(&types_store, &scratch, v1); try std.testing.expectEqual(.not_recursive, result); @@ -459,7 +414,7 @@ test "occurs: tuple recursion (v = Tuple(v, Str))" { defer scratch.deinit(); const v = try types_store.fresh(); - const str_var = try types_store.freshFromContent(Content{ .structure = .str }); + const str_var = try types_store.freshFromContent(Content{ .structure = .empty_record }); const elems_range = try types_store.appendVars(&[_]Var{ v, str_var }); const tuple = types.Tuple{ .elems = elems_range }; @@ -482,7 +437,7 @@ test "occurs: tuple not recursive (v = Tuple(Str, Str))" { var scratch = try Scratch.init(gpa); defer scratch.deinit(); - const str_var = try types_store.freshFromContent(Content{ .structure = .str }); + const str_var = try types_store.freshFromContent(Content{ .structure = .empty_record }); const elems_range = try types_store.appendVars(&[_]Var{ str_var, str_var }); const tuple = types.Tuple{ .elems = elems_range }; @@ -530,8 +485,8 @@ test "occurs: alias with no recursion (v = Alias Str)" { defer scratch.deinit(); const alias_var = try types_store.fresh(); - const backing_var = try types_store.freshFromContent(Content{ .structure = .str }); - const arg_var = try types_store.freshFromContent(Content{ .structure = .str }); + const backing_var = try types_store.freshFromContent(Content{ .structure = .empty_record }); + const arg_var = try types_store.freshFromContent(Content{ .structure = .empty_record }); try types_store.setRootVarContent(alias_var, try types_store.mkAlias( types.TypeIdent{ .ident_idx = undefined }, @@ -588,9 +543,16 @@ test "occurs: nested recursive tag union (v = [ Cons(elem, Box(v)) ] )" { const linked_list = try types_store.fresh(); const elem = try types_store.fresh(); - // Wrap the recursive var in a Box to simulate nesting + // Wrap the recursive var in a nominal Box to simulate nesting const boxed_linked_list = try types_store.fresh(); - try types_store.setRootVarContent(boxed_linked_list, .{ .structure = .{ .box = linked_list } }); + const box_backing_var = try types_store.freshFromContent(.{ .structure = .empty_record }); + try types_store.setVarContent(boxed_linked_list, try types_store.mkNominal( + undefined, + box_backing_var, + &.{linked_list}, + Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 0 }, + false, + )); // Build tag args: (elem, Box(linked_list)) const cons_tag_args = try types_store.appendVars(&[_]Var{ elem, boxed_linked_list }); @@ -639,6 +601,7 @@ test "occurs: recursive tag union (v = List: [ Cons(Elem, List), Nil ])" { backing_var, &.{}, Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 0 }, + false, )); // assert that starting from the nominal type, it works @@ -704,6 +667,7 @@ test "occurs: recursive tag union with multiple nominals (TypeA := TypeB, TypeB type_b_backing, &.{}, Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 0 }, + false, )); // Set up TypeA = Type B @@ -712,6 +676,7 @@ test "occurs: recursive tag union with multiple nominals (TypeA := TypeB, TypeB type_b_nominal, &.{}, Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 0 }, + false, )); // assert that starting from the `TypeA` nominal, it works diff --git a/src/check/problem.zig b/src/check/problem.zig index b443d14b41..d28e6f05a8 100644 --- a/src/check/problem.zig +++ b/src/check/problem.zig @@ -32,25 +32,75 @@ const SnapshotContentIdx = snapshot.SnapshotContentIdx; const Var = types_mod.Var; const Content = types_mod.Content; +/// Returns singular form if count is 1, plural form otherwise. +/// Usage: pluralize(count, "argument", "arguments") +fn pluralize(count: anytype, singular: []const u8, plural: []const u8) []const u8 { + return if (count == 1) singular else plural; +} + /// The kind of problem we're dealing with pub const Problem = union(enum) { type_mismatch: TypeMismatch, + fn_call_arity_mismatch: FnCallArityMismatch, type_apply_mismatch_arities: TypeApplyArityMismatch, + static_dispach: StaticDispatch, + cannot_access_opaque_nominal: CannotAccessOpaqueNominal, + nominal_type_resolution_failed: NominalTypeResolutionFailed, number_does_not_fit: NumberDoesNotFit, negative_unsigned_int: NegativeUnsignedInt, - infinite_recursion: struct { var_: Var }, - anonymous_recursion: struct { var_: Var }, - invalid_number_type: VarProblem1, - invalid_record_ext: VarProblem1, - invalid_tag_union_ext: VarProblem1, + invalid_numeric_literal: InvalidNumericLiteral, + unused_value: UnusedValue, + recursive_alias: RecursiveAlias, + unsupported_alias_where_clause: UnsupportedAliasWhereClause, + infinite_recursion: VarWithSnapshot, + anonymous_recursion: VarWithSnapshot, + invalid_number_type: VarWithSnapshot, + invalid_record_ext: VarWithSnapshot, + invalid_tag_union_ext: VarWithSnapshot, + platform_def_not_found: PlatformDefNotFound, + platform_alias_not_found: PlatformAliasNotFound, + comptime_crash: ComptimeCrash, + comptime_expect_failed: ComptimeExpectFailed, + comptime_eval_error: ComptimeEvalError, bug: Bug, pub const Idx = enum(u32) { _ }; pub const Tag = std.meta.Tag(@This()); }; -/// A single var problem -pub const VarProblem1 = struct { +/// Error for when a platform expects an alias to be defined, but it's not there +pub const PlatformAliasNotFound = struct { + expected_alias_ident: Ident.Idx, + ctx: enum { not_found, found_but_not_alias }, +}; + +/// Error for when a platform expects an alias to be defined, but it's not there +pub const PlatformDefNotFound = struct { + expected_def_ident: Ident.Idx, + ctx: enum { not_found, found_but_not_exported }, +}; + +/// A crash that occurred during compile-time evaluation +pub const ComptimeCrash = struct { + message: []const u8, + region: base.Region, +}; + +/// An expect that failed during compile-time evaluation +pub const ComptimeExpectFailed = struct { + message: []const u8, + region: base.Region, +}; + +/// An error that occurred during compile-time evaluation +pub const ComptimeEvalError = struct { + error_name: []const u8, + region: base.Region, +}; + +/// A problem involving a single type variable, with a snapshot for error reporting. +/// Used for recursion errors, invalid extension types, etc. +pub const VarWithSnapshot = struct { var_: Var, snapshot: SnapshotContentIdx, }; @@ -69,6 +119,20 @@ pub const NegativeUnsignedInt = struct { expected_type: SnapshotContentIdx, }; +/// Invalid numeric literal that cannot be converted to target type +pub const InvalidNumericLiteral = struct { + literal_var: Var, + expected_type: SnapshotContentIdx, + is_fractional: bool, + region: base.Region, +}; + +/// Error when a stmt expression returns a non-empty record value +pub const UnusedValue = struct { + var_: Var, + snapshot: SnapshotContentIdx, +}; + // type mismatch // /// These two variables mismatch. This should usually be cast into a more @@ -96,10 +160,14 @@ pub const TypeMismatchDetail = union(enum) { incompatible_list_elements: IncompatibleListElements, incompatible_if_cond, incompatible_if_branches: IncompatibleIfBranches, + incompatible_match_cond_pattern: IncompatibleMatchCondPattern, incompatible_match_patterns: IncompatibleMatchPatterns, incompatible_match_branches: IncompatibleMatchBranches, invalid_bool_binop: InvalidBoolBinop, invalid_nominal_tag, + invalid_nominal_record, + invalid_nominal_tuple, + invalid_nominal_value, cross_module_import: CrossModuleImport, incompatible_fn_call_arg: IncompatibleFnCallArg, incompatible_fn_args_bound_var: IncompatibleFnArgsBoundVar, @@ -107,7 +175,7 @@ pub const TypeMismatchDetail = union(enum) { /// Problem data for when list elements have incompatible types pub const IncompatibleListElements = struct { - last_elem_expr: CIR.Expr.Idx, + last_elem_idx: CIR.Node.Idx, incompatible_elem_index: u32, // 0-based index of the incompatible element list_length: u32, // Total number of elements in the list }; @@ -118,6 +186,16 @@ pub const CrossModuleImport = struct { module_idx: CIR.Import.Idx, }; +/// Problem data when function is called with wrong number of arguments +pub const FnCallArityMismatch = struct { + fn_name: ?Ident.Idx, + fn_var: Var, + fn_snapshot: SnapshotContentIdx, + call_region: base.Region, + expected_args: u32, + actual_args: u32, +}; + /// Problem data when function argument types don't match pub const IncompatibleFnCallArg = struct { fn_name: ?Ident.Idx, @@ -144,6 +222,11 @@ pub const IncompatibleIfBranches = struct { problem_branch_index: u32, }; +/// Problem data for when match expression type is incompatible from the patterns +pub const IncompatibleMatchCondPattern = struct { + match_expr: CIR.Expr.Idx, +}; + /// Problem data for when match patterns have have incompatible types pub const IncompatibleMatchPatterns = struct { match_expr: CIR.Expr.Idx, @@ -167,17 +250,87 @@ pub const InvalidBoolBinop = struct { binop: enum { @"and", @"or" }, }; +// static dispatch // + +/// Error related to static dispatch +pub const StaticDispatch = union(enum) { + dispatcher_not_nominal: DispatcherNotNominal, + dispatcher_does_not_impl_method: DispatcherDoesNotImplMethod, + type_does_not_support_equality: TypeDoesNotSupportEquality, +}; + +/// Error when you try to static dispatch on something that's not a nominal type +pub const DispatcherNotNominal = struct { + dispatcher_var: Var, + dispatcher_snapshot: SnapshotContentIdx, + fn_var: Var, + method_name: Ident.Idx, +}; + +/// Error when you try to static dispatch but the dispatcher does not have that method +pub const DispatcherDoesNotImplMethod = struct { + dispatcher_var: Var, + dispatcher_snapshot: SnapshotContentIdx, + dispatcher_type: DispatcherType, + fn_var: Var, + method_name: Ident.Idx, + origin: types_mod.StaticDispatchConstraint.Origin, + + /// Type of the dispatcher + pub const DispatcherType = enum { nominal, rigid }; +}; + +/// Error when an anonymous type (record, tuple, tag union) doesn't support equality +/// because one or more of its components contain types that don't have is_eq +pub const TypeDoesNotSupportEquality = struct { + dispatcher_var: Var, + dispatcher_snapshot: SnapshotContentIdx, + fn_var: Var, +}; + +// bug // + +/// Error when you try to use an opaque nominal type constructor +pub const CannotAccessOpaqueNominal = struct { + var_: Var, + nominal_type_name: Ident.Idx, +}; + +/// Compiler bug: a nominal type variable doesn't resolve to a nominal_type structure. +/// This should never happen because: +/// 1. The canonicalizer only creates nominal patterns/expressions for s_nominal_decl statements +/// 2. generateNominalDecl always sets the decl_var to a nominal_type structure +/// 3. instantiateVar and copyVar preserve the nominal_type structure +pub const NominalTypeResolutionFailed = struct { + var_: Var, + nominal_type_decl_var: Var, +}; + // bug // /// Error when you try to apply the wrong number of arguments to a type in /// an annotation pub const TypeApplyArityMismatch = struct { type_name: base.Ident.Idx, - anno_var: Var, + region: base.Region, num_expected_args: u32, num_actual_args: u32, }; +/// Error when a type alias references itself (aliases cannot be recursive) +/// Use nominal types (:=) for recursive types instead +pub const RecursiveAlias = struct { + type_name: base.Ident.Idx, + region: base.Region, +}; + +/// Error when using alias syntax in where clause (e.g., `where [a.SomeAlias]`) +/// This syntax was used for abilities which have been removed from the language +pub const UnsupportedAliasWhereClause = struct { + alias_name: base.Ident.Idx, + region: base.Region, +}; + // bug // /// A bug that occurred during unification @@ -195,13 +348,14 @@ pub const ReportBuilder = struct { const Self = @This(); gpa: Allocator, - buf: std.ArrayList(u8), + bytes_buf: std.array_list.Managed(u8), module_env: *ModuleEnv, can_ir: *const ModuleEnv, snapshots: *const snapshot.Store, source: []const u8, filename: []const u8, other_modules: []const *const ModuleEnv, + import_mapping: *const @import("types").import_mapping.ImportMapping, /// Init report builder /// Only owned field is `buf` @@ -212,13 +366,15 @@ pub const ReportBuilder = struct { snapshots: *const snapshot.Store, filename: []const u8, other_modules: []const *const ModuleEnv, + import_mapping: *const @import("types").import_mapping.ImportMapping, ) Self { return .{ .gpa = gpa, - .buf = std.ArrayList(u8).init(gpa), + .bytes_buf = std.array_list.Managed(u8).init(gpa), .module_env = module_env, .can_ir = can_ir, .snapshots = snapshots, + .import_mapping = import_mapping, .source = module_env.common.source, .filename = filename, .other_modules = other_modules, @@ -228,7 +384,34 @@ pub const ReportBuilder = struct { /// Deinit report builder /// Only owned field is `buf` pub fn deinit(self: *Self) void { - self.buf.deinit(); + self.bytes_buf.deinit(); + } + + /// Get the formatted string for a snapshot. + /// Returns a placeholder if the formatted string is missing, allowing error reporting + /// to continue gracefully even if snapshots are incomplete. + fn getFormattedString(self: *const Self, idx: SnapshotContentIdx) []const u8 { + return self.snapshots.getFormattedString(idx) orelse ""; + } + + /// Returns the operator symbol for a given method ident, or null if not an operator method. + /// Maps method idents like plus, minus, times, div_by to their corresponding operator symbols. + fn getOperatorForMethod(self: *const Self, method_ident: Ident.Idx) ?[]const u8 { + const idents = self.can_ir.idents; + if (method_ident == idents.plus) return "+"; + if (method_ident == idents.minus) return "-"; + if (method_ident == idents.times) return "*"; + if (method_ident == idents.div_by) return "/"; + if (method_ident == idents.div_trunc_by) return "//"; + if (method_ident == idents.rem_by) return "%"; + if (method_ident == idents.negate) return "-"; + if (method_ident == idents.is_eq) return "=="; + if (method_ident == idents.is_lt) return "<"; + if (method_ident == idents.is_lte) return "<="; + if (method_ident == idents.is_gt) return ">"; + if (method_ident == idents.is_gte) return ">="; + if (method_ident == idents.not) return "not"; + return null; } /// Build a report for a problem @@ -239,69 +422,109 @@ pub const ReportBuilder = struct { const trace = tracy.trace(@src()); defer trace.end(); - var snapshot_writer = snapshot.SnapshotWriter.initWithContext( - self.buf.writer(), - self.snapshots, - self.module_env.getIdentStore(), - self.can_ir.module_name, - self.can_ir, - self.other_modules, - ); - switch (problem) { .type_mismatch => |mismatch| { if (mismatch.detail) |detail| { switch (detail) { .incompatible_list_elements => |data| { - return self.buildIncompatibleListElementsReport(&snapshot_writer, mismatch.types, data); + return self.buildIncompatibleListElementsReport(mismatch.types, data); }, .incompatible_if_cond => { - return self.buildInvalidIfCondition(&snapshot_writer, mismatch.types); + return self.buildInvalidIfCondition(mismatch.types); }, .incompatible_if_branches => |data| { - return self.buildIncompatibleIfBranches(&snapshot_writer, mismatch.types, data); + return self.buildIncompatibleIfBranches(mismatch.types, data); + }, + .incompatible_match_cond_pattern => |data| { + return self.buildIncompatibleMatchCondPattern(mismatch.types, data); }, .incompatible_match_patterns => |data| { - return self.buildIncompatibleMatchPatterns(&snapshot_writer, mismatch.types, data); + return self.buildIncompatibleMatchPatterns(mismatch.types, data); }, .incompatible_match_branches => |data| { - return self.buildIncompatibleMatchBranches(&snapshot_writer, mismatch.types, data); + return self.buildIncompatibleMatchBranches(mismatch.types, data); }, .invalid_bool_binop => |data| { - return self.buildInvalidBoolBinop(&snapshot_writer, mismatch.types, data); + return self.buildInvalidBoolBinop(mismatch.types, data); }, .invalid_nominal_tag => { - return self.buildInvalidNominalTag(&snapshot_writer, mismatch.types); + return self.buildInvalidNominalTag(mismatch.types); + }, + .invalid_nominal_record => { + return self.buildInvalidNominalRecord(mismatch.types); + }, + .invalid_nominal_tuple => { + return self.buildInvalidNominalTuple(mismatch.types); + }, + .invalid_nominal_value => { + return self.buildInvalidNominalValue(mismatch.types); }, .cross_module_import => |data| { - return self.buildCrossModuleImportError(&snapshot_writer, mismatch.types, data); + return self.buildCrossModuleImportError(mismatch.types, data); }, .incompatible_fn_call_arg => |data| { - return self.buildIncompatibleFnCallArg(&snapshot_writer, mismatch.types, data); + return self.buildIncompatibleFnCallArg(mismatch.types, data); }, .incompatible_fn_args_bound_var => |data| { - return self.buildIncompatibleFnArgsBoundVar(&snapshot_writer, mismatch.types, data); + return self.buildIncompatibleFnArgsBoundVar(mismatch.types, data); }, } } else { - return self.buildGenericTypeMismatchReport(&snapshot_writer, mismatch.types); + return self.buildGenericTypeMismatchReport(mismatch.types); } }, + .fn_call_arity_mismatch => |data| { + return self.buildFnCallArityMismatchReport(data); + }, .type_apply_mismatch_arities => |data| { - return self.buildTypeApplyArityMismatchReport(&snapshot_writer, data); + return self.buildTypeApplyArityMismatchReport(data); + }, + .cannot_access_opaque_nominal => |data| { + return self.buildCannotAccessOpaqueNominal(data); + }, + .nominal_type_resolution_failed => |data| { + return self.buildNominalTypeResolutionFailed(data); + }, + .static_dispach => |detail| { + switch (detail) { + .dispatcher_not_nominal => |data| return self.buildStaticDispatchDispatcherNotNominal(data), + .dispatcher_does_not_impl_method => |data| return self.buildStaticDispatchDispatcherDoesNotImplMethod(data), + .type_does_not_support_equality => |data| return self.buildTypeDoesNotSupportEquality(data), + } }, .number_does_not_fit => |data| { - return self.buildNumberDoesNotFitReport(&snapshot_writer, data); + return self.buildNumberDoesNotFitReport(data); }, .negative_unsigned_int => |data| { - return self.buildNegativeUnsignedIntReport(&snapshot_writer, data); + return self.buildNegativeUnsignedIntReport(data); }, - .infinite_recursion => |_| return self.buildUnimplementedReport(), - .anonymous_recursion => |_| return self.buildUnimplementedReport(), - .invalid_number_type => |_| return self.buildUnimplementedReport(), - .invalid_record_ext => |_| return self.buildUnimplementedReport(), - .invalid_tag_union_ext => |_| return self.buildUnimplementedReport(), - .bug => |_| return self.buildUnimplementedReport(), + .invalid_numeric_literal => |data| { + return self.buildInvalidNumericLiteralReport(data); + }, + .unused_value => |data| { + return self.buildUnusedValueReport(data); + }, + .recursive_alias => |data| { + return self.buildRecursiveAliasReport(data); + }, + .unsupported_alias_where_clause => |data| { + return self.buildUnsupportedAliasWhereClauseReport(data); + }, + .infinite_recursion => |_| return self.buildUnimplementedReport("infinite_recursion"), + .anonymous_recursion => |_| return self.buildUnimplementedReport("anonymous_recursion"), + .invalid_number_type => |_| return self.buildUnimplementedReport("invalid_number_type"), + .invalid_record_ext => |_| return self.buildUnimplementedReport("invalid_record_ext"), + .invalid_tag_union_ext => |_| return self.buildUnimplementedReport("invalid_tag_union_ext"), + .platform_alias_not_found => |data| { + return self.buildPlatformAliasNotFound(data); + }, + .platform_def_not_found => |data| { + return self.buildPlatformDefNotFound(data); + }, + .comptime_crash => |data| return self.buildComptimeCrashReport(data), + .comptime_expect_failed => |data| return self.buildComptimeExpectFailedReport(data), + .comptime_eval_error => |data| return self.buildComptimeEvalErrorReport(data), + .bug => |_| return self.buildUnimplementedReport("bug"), } } @@ -310,19 +533,13 @@ pub const ReportBuilder = struct { /// Build a report for type mismatch diagnostic fn buildGenericTypeMismatchReport( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, types: TypePair, ) !Report { var report = Report.init(self.gpa, "TYPE MISMATCH", .runtime_error); errdefer report.deinit(); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.actual_snapshot); - const owned_actual = try report.addOwnedString(self.buf.items[0..]); - - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const owned_expected = try report.addOwnedString(self.buf.items[0..]); + const owned_actual = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); + const owned_expected = try report.addOwnedString(self.getFormattedString(types.expected_snapshot)); // For annotation mismatches, we want to highlight the expression that doesn't match, // not the annotation itself. When from_annotation is true and we're showing @@ -331,31 +548,26 @@ pub const ReportBuilder = struct { const region_var = if (types.constraint_origin_var) |origin_var| origin_var - else if (types.from_annotation) - types.expected_var // Use expected_var to highlight the expression causing the mismatch else types.actual_var; const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(region_var))); // Check if both types are functions to provide more specific error messages - if (types.from_annotation) { - // Check the snapshot content to determine if we have function types - const expected_content = self.snapshots.getContent(types.expected_snapshot); - const actual_content = self.snapshots.getContent(types.actual_snapshot); + const expected_content = self.snapshots.getContent(types.expected_snapshot); + const actual_content = self.snapshots.getContent(types.actual_snapshot); - if (self.areBothFunctionSnapshots(expected_content, actual_content)) { - // When we have constraint_origin_var, it indicates this error originated from - // a specific constraint like a dot access (e.g., str.to_utf8()). - // In this case, show a specialized argument type mismatch error. - if (types.constraint_origin_var) |origin_var| { - report.deinit(); - return self.buildIncompatibleFnCallArg(snapshot_writer, types, .{ - .fn_name = null, - .arg_var = origin_var, - .incompatible_arg_index = 0, // First argument - .num_args = 1, // Single argument lambda - }); - } + if (types.from_annotation and areBothFunctionSnapshots(expected_content, actual_content)) { + // When we have constraint_origin_var, it indicates this error originated from + // a specific constraint like a dot access (e.g., str.to_utf8()). + // In this case, show a specialized argument type mismatch error. + if (types.constraint_origin_var) |origin_var| { + report.deinit(); + return self.buildIncompatibleFnCallArg(types, .{ + .fn_name = null, + .arg_var = origin_var, + .incompatible_arg_index = 0, // First argument + .num_args = 1, // Single argument lambda + }); } } @@ -374,44 +586,21 @@ pub const ReportBuilder = struct { ); try report.document.addLineBreak(); + try report.document.addText("It has the type:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(owned_actual); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + if (types.from_annotation) { - try report.document.addText("The type annotation says it should have the type:"); + try report.document.addText("But the type annotation says it should have the type:"); } else { - try report.document.addText("It has the type:"); + try report.document.addText("But I expected it to be:"); } try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(owned_actual, .type_variable); try report.document.addLineBreak(); - try report.document.addLineBreak(); - - try report.document.addText("But here it's being used as:"); - try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(owned_expected, .type_variable); - - // Add a hint if this looks like a numeric literal size issue - const actual_str = owned_actual; - const expected_str = owned_expected; - - // Check if we're dealing with numeric types - const is_numeric_issue = (std.mem.indexOf(u8, actual_str, "Num(") != null and - std.mem.indexOf(u8, expected_str, "Num(") != null); - - // Check if target is a concrete integer type - const has_unsigned = std.mem.indexOf(u8, expected_str, "Unsigned") != null; - const has_signed = std.mem.indexOf(u8, expected_str, "Signed") != null; - - if (is_numeric_issue and (has_unsigned or has_signed)) { - try report.document.addLineBreak(); - try report.document.addLineBreak(); - try report.document.addAnnotated("Hint:", .emphasized); - if (has_unsigned) { - try report.document.addReflowingText(" This might be because the numeric literal is either negative or too large to fit in the unsigned type."); - } else { - try report.document.addReflowingText(" This might be because the numeric literal is too large to fit in the target type."); - } - } + try report.document.addCodeBlock(owned_expected); return report; } @@ -419,7 +608,6 @@ pub const ReportBuilder = struct { /// Build a report for incompatible list elements fn buildIncompatibleListElementsReport( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, types: TypePair, data: IncompatibleListElements, ) !Report { @@ -427,21 +615,16 @@ pub const ReportBuilder = struct { errdefer report.deinit(); // Create owned strings - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const expected_type = try report.addOwnedString(self.buf.items); + const expected_type = try report.addOwnedString(self.getFormattedString(types.expected_snapshot)); + const actual_type = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.actual_snapshot); - const actual_type = try report.addOwnedString(self.buf.items); + self.bytes_buf.clearRetainingCapacity(); + try appendOrdinal(&self.bytes_buf, data.incompatible_elem_index); + const expected_type_ordinal = try report.addOwnedString(self.bytes_buf.items); - self.buf.clearRetainingCapacity(); - try appendOrdinal(&self.buf, data.incompatible_elem_index); - const expected_type_ordinal = try report.addOwnedString(self.buf.items); - - self.buf.clearRetainingCapacity(); - try appendOrdinal(&self.buf, data.incompatible_elem_index + 1); - const actual_type_ordinal = try report.addOwnedString(self.buf.items); + self.bytes_buf.clearRetainingCapacity(); + try appendOrdinal(&self.bytes_buf, data.incompatible_elem_index + 1); + const actual_type_ordinal = try report.addOwnedString(self.bytes_buf.items); // Add description if (data.list_length == 2) { @@ -461,7 +644,7 @@ pub const ReportBuilder = struct { // Determine the overall region that encompasses both elements const actual_region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(types.actual_var))); - const expected_region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.last_elem_expr))); + const expected_region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.last_elem_idx))); const overall_start_offset = @min(actual_region.start.offset, expected_region.start.offset); const overall_end_offset = @max(actual_region.end.offset, expected_region.end.offset); @@ -524,8 +707,8 @@ pub const ReportBuilder = struct { try report.document.addText(expected_type_ordinal); try report.document.addText(" element has this type:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(expected_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(expected_type); try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -534,8 +717,8 @@ pub const ReportBuilder = struct { try report.document.addText(actual_type_ordinal); try report.document.addText(" element has this type:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(actual_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_type); try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -555,16 +738,13 @@ pub const ReportBuilder = struct { /// Build a report for incompatible list elements fn buildInvalidIfCondition( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, types: TypePair, ) !Report { var report = Report.init(self.gpa, "INVALID IF CONDITION", .runtime_error); errdefer report.deinit(); // Create owned strings - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.actual_snapshot); - const actual_type = try report.addOwnedString(self.buf.items); + const actual_type = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); // Add description try report.document.addText("This "); @@ -611,8 +791,8 @@ pub const ReportBuilder = struct { // Add description try report.document.addText("Right now, it has the type:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(actual_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_type); try report.document.addLineBreak(); // Add explanation @@ -633,7 +813,6 @@ pub const ReportBuilder = struct { /// Build a report for incompatible list elements fn buildIncompatibleIfBranches( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, types: TypePair, data: IncompatibleIfBranches, ) !Report { @@ -648,17 +827,12 @@ pub const ReportBuilder = struct { errdefer report.deinit(); // Create owned strings - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.actual_snapshot); - const actual_type = try report.addOwnedString(self.buf.items); + const actual_type = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); + const expected_type = try report.addOwnedString(self.getFormattedString(types.expected_snapshot)); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const expected_type = try report.addOwnedString(self.buf.items); - - self.buf.clearRetainingCapacity(); - try appendOrdinal(&self.buf, data.problem_branch_index + 1); - const branch_ordinal = try report.addOwnedString(self.buf.items); + self.bytes_buf.clearRetainingCapacity(); + try appendOrdinal(&self.bytes_buf, data.problem_branch_index + 1); + const branch_ordinal = try report.addOwnedString(self.bytes_buf.items); // Add title if (is_only_if_else) { @@ -740,8 +914,8 @@ pub const ReportBuilder = struct { try report.document.addText(" branch has this type:"); } try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(actual_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_type); try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -756,8 +930,8 @@ pub const ReportBuilder = struct { try report.document.addText("But all the previous branches have this type:"); } try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(expected_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(expected_type); try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -776,10 +950,94 @@ pub const ReportBuilder = struct { return report; } + /// Build a report for the pattern in a match is a different type than the + /// match condition expr. e.g. match True { "hello" => ... } + fn buildIncompatibleMatchCondPattern( + self: *Self, + types: TypePair, + data: IncompatibleMatchCondPattern, + ) !Report { + var report = Report.init(self.gpa, "INCOMPATIBLE MATCH PATTERNS", .runtime_error); + errdefer report.deinit(); + + // Create owned strings + const actual_type = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); + const expected_type = try report.addOwnedString(self.getFormattedString(types.expected_snapshot)); + + try report.document.addText("The first pattern in this "); + try report.document.addAnnotated("match", .keyword); + try report.document.addText(" is incompatible:"); + try report.document.addLineBreak(); + + // Determine the overall region that encompasses both elements + const match_expr_region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.match_expr))); + const overall_region_info = base.RegionInfo.position( + self.source, + self.module_env.getLineStarts(), + match_expr_region.start.offset, + match_expr_region.end.offset, + ) catch return report; + + // Get region info for invalid branch + const invalid_var_region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(types.actual_var))); + const invalid_var_region_info = base.RegionInfo.position( + self.source, + self.module_env.getLineStarts(), + invalid_var_region.start.offset, + invalid_var_region.end.offset, + ) catch return report; + + // Create the display region + const display_region = SourceCodeDisplayRegion{ + .line_text = self.gpa.dupe(u8, overall_region_info.calculateLineText(self.source, self.module_env.getLineStarts())) catch return report, + .start_line = overall_region_info.start_line_idx + 1, + .start_column = overall_region_info.start_col_idx + 1, + .end_line = overall_region_info.end_line_idx + 1, + .end_column = overall_region_info.end_col_idx + 1, + .region_annotation = .dimmed, + .filename = self.filename, + }; + + // Create underline regions + const underline_regions = [_]UnderlineRegion{ + .{ + .start_line = invalid_var_region_info.start_line_idx + 1, + .start_column = invalid_var_region_info.start_col_idx + 1, + .end_line = invalid_var_region_info.end_line_idx + 1, + .end_column = invalid_var_region_info.end_col_idx + 1, + .annotation = .error_highlight, + }, + }; + + try report.document.addSourceCodeWithUnderlines(display_region, &underline_regions); + try report.document.addLineBreak(); + + // Show the type of the invalid branch + try report.document.addText("The first pattern has the type:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_type); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + // Show the type of the other branches + try report.document.addText("But the expression between the "); + try report.document.addAnnotated("match", .keyword); + try report.document.addText(" parenthesis has the type:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(expected_type); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("These two types can never match!"); + + return report; + } + /// Build a report for incompatible match branches fn buildIncompatibleMatchPatterns( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, types: TypePair, data: IncompatibleMatchPatterns, ) !Report { @@ -787,25 +1045,19 @@ pub const ReportBuilder = struct { errdefer report.deinit(); // Create owned strings - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.actual_snapshot); - const actual_type = try report.addOwnedString(self.buf.items); + const actual_type = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); + const expected_type = try report.addOwnedString(self.getFormattedString(types.expected_snapshot)); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const expected_type = try report.addOwnedString(self.buf.items); + self.bytes_buf.clearRetainingCapacity(); + try appendOrdinal(&self.bytes_buf, data.problem_branch_index + 1); + const branch_ord = try report.addOwnedString(self.bytes_buf.items); - self.buf.clearRetainingCapacity(); - try appendOrdinal(&self.buf, data.problem_branch_index + 1); - const branch_ord = try report.addOwnedString(self.buf.items); - - self.buf.clearRetainingCapacity(); - try appendOrdinal(&self.buf, data.problem_pattern_index + 1); - const pattern_ord = try report.addOwnedString(self.buf.items); + self.bytes_buf.clearRetainingCapacity(); + try appendOrdinal(&self.bytes_buf, data.problem_pattern_index + 1); + const pattern_ord = try report.addOwnedString(self.bytes_buf.items); // Add description if (data.num_patterns > 1) { - self.buf.clearRetainingCapacity(); try report.document.addText("The pattern "); try report.document.addText(pattern_ord); try report.document.addText(" pattern in this "); @@ -814,7 +1066,6 @@ pub const ReportBuilder = struct { try report.document.addText(" differs from previous ones:"); try report.document.addLineBreak(); } else { - self.buf.clearRetainingCapacity(); try report.document.addText("The pattern in the "); try report.document.addText(branch_ord); try report.document.addText(" branch of this "); @@ -867,13 +1118,12 @@ pub const ReportBuilder = struct { try report.document.addLineBreak(); // Show the type of the invalid branch - self.buf.clearRetainingCapacity(); try report.document.addText("The "); try report.document.addText(branch_ord); try report.document.addText(" pattern has this type:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(actual_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_type); try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -884,8 +1134,8 @@ pub const ReportBuilder = struct { try report.document.addText("But the other pattern has this type:"); } try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(expected_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(expected_type); try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -893,8 +1143,6 @@ pub const ReportBuilder = struct { try report.document.addText("All patterns in an "); try report.document.addAnnotated("match", .keyword); try report.document.addText(" must have compatible types."); - try report.document.addLineBreak(); - try report.document.addLineBreak(); return report; } @@ -902,7 +1150,6 @@ pub const ReportBuilder = struct { /// Build a report for incompatible match branches fn buildIncompatibleMatchBranches( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, types: TypePair, data: IncompatibleMatchBranches, ) !Report { @@ -913,26 +1160,19 @@ pub const ReportBuilder = struct { errdefer report.deinit(); // Create owned strings - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.actual_snapshot); - const actual_type = try report.addOwnedString(self.buf.items); + const actual_type = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); + const expected_type = try report.addOwnedString(self.getFormattedString(types.expected_snapshot)); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const expected_type = try report.addOwnedString(self.buf.items); - - self.buf.clearRetainingCapacity(); - try appendOrdinal(&self.buf, data.problem_branch_index + 1); - const branch_ord = try report.addOwnedString(self.buf.items); + self.bytes_buf.clearRetainingCapacity(); + try appendOrdinal(&self.bytes_buf, data.problem_branch_index + 1); + const branch_ord = try report.addOwnedString(self.bytes_buf.items); // Add description if (data.num_branches == 2) { - self.buf.clearRetainingCapacity(); try report.document.addText("The second branch's type in this "); try report.document.addAnnotated("match", .keyword); try report.document.addText(" is different from the first branch:"); } else { - self.buf.clearRetainingCapacity(); try report.document.addText("The "); try report.document.addText(branch_ord); try report.document.addText(" branch's type in this "); @@ -991,8 +1231,8 @@ pub const ReportBuilder = struct { try report.document.addText(branch_ord); try report.document.addText(" branch has this type;"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(actual_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_type); try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -1005,8 +1245,8 @@ pub const ReportBuilder = struct { try report.document.addText("But the previous branch has this type:"); } try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(expected_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(expected_type); try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -1028,7 +1268,6 @@ pub const ReportBuilder = struct { /// Build a report for incompatible match branches fn buildInvalidBoolBinop( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, types: TypePair, data: InvalidBoolBinop, ) !Report { @@ -1036,9 +1275,7 @@ pub const ReportBuilder = struct { errdefer report.deinit(); // Create owned strings - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.actual_snapshot); - const actual_type = try report.addOwnedString(self.buf.items); + const actual_type = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); // Add description try report.document.addText("I'm having trouble with this bool operation:"); @@ -1104,8 +1341,8 @@ pub const ReportBuilder = struct { } try report.document.addText(" side is:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(actual_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_type); try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -1120,7 +1357,6 @@ pub const ReportBuilder = struct { /// Build a report for incompatible match branches fn buildInvalidNominalTag( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, types: TypePair, ) !Report { var report = Report.init(self.gpa, "INVALID NOMINAL TAG", .runtime_error); @@ -1132,9 +1368,9 @@ pub const ReportBuilder = struct { std.debug.assert(actual_content.structure == .tag_union); std.debug.assert(actual_content.structure.tag_union.tags.len() == 1); const actual_tag = self.snapshots.tags.get(actual_content.structure.tag_union.tags.start); - self.buf.clearRetainingCapacity(); - try snapshot_writer.writeTag(actual_tag, types.actual_snapshot); - const actual_tag_str = try report.addOwnedString(self.buf.items); + const actual_tag_formatted = try self.snapshots.formatTagString(self.gpa, actual_tag, self.module_env.getIdentStore()); + defer self.gpa.free(actual_tag_formatted); + const actual_tag_str = try report.addOwnedString(actual_tag_formatted); // Create expected tag str const expected_content = self.snapshots.getContent(types.expected_snapshot); @@ -1160,55 +1396,47 @@ pub const ReportBuilder = struct { // Show the invalid tag try report.document.addText("The tag is:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(actual_tag_str, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_tag_str); try report.document.addLineBreak(); try report.document.addLineBreak(); // Show the expected tags if (expected_num_tags_str == 1) { const expected_tag = self.snapshots.tags.get(expected_content.structure.tag_union.tags.start); - self.buf.clearRetainingCapacity(); - try snapshot_writer.writeTag(expected_tag, types.expected_snapshot); - const expected_tag_str = try report.addOwnedString(self.buf.items); + const expected_tag_formatted = try self.snapshots.formatTagString(self.gpa, expected_tag, self.module_env.getIdentStore()); + defer self.gpa.free(expected_tag_formatted); + const expected_tag_str = try report.addOwnedString(expected_tag_formatted); - try report.document.addText("But it should be:"); + try report.document.addText("But the nominal type needs it to be:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(expected_tag_str, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(expected_tag_str); } else { - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const expected_type = try report.addOwnedString(self.buf.items); + const expected_type = try report.addOwnedString(self.getFormattedString(types.expected_snapshot)); - try report.document.addText("But it should be one of:"); + try report.document.addText("But the nominal type needs it to one of:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(expected_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(expected_type); // Check if there's a tag with the same name in the list of possible tags - const actual_tag_name_str = self.can_ir.getIdent(actual_tag.name); var iter = expected_content.structure.tag_union.tags.iterIndices(); while (iter.next()) |tag_index| { const cur_expected_tag = self.snapshots.tags.get(tag_index); - const expected_tag_name_str = self.can_ir.getIdent(cur_expected_tag.name); - if (std.mem.eql(u8, actual_tag_name_str, expected_tag_name_str)) { - snapshot_writer.resetContext(); - - self.buf.clearRetainingCapacity(); - try snapshot_writer.writeTag(cur_expected_tag, types.expected_snapshot); - const cur_expected_tag_str = try report.addOwnedString(self.buf.items); + if (actual_tag.name == cur_expected_tag.name) { + const cur_expected_tag_formatted = try self.snapshots.formatTagString(self.gpa, cur_expected_tag, self.module_env.getIdentStore()); + defer self.gpa.free(cur_expected_tag_formatted); + const cur_expected_tag_str = try report.addOwnedString(cur_expected_tag_formatted); try report.document.addLineBreak(); try report.document.addLineBreak(); try report.document.addAnnotated("Hint:", .emphasized); try report.document.addReflowingText(" The nominal type has a tag with the same name, but different args:"); try report.document.addLineBreak(); - try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(cur_expected_tag_str, .type_variable); + try report.document.addCodeBlock(cur_expected_tag_str); break; } @@ -1218,31 +1446,145 @@ pub const ReportBuilder = struct { return report; } + /// Build a report for invalid nominal record (record fields don't match) + fn buildInvalidNominalRecord( + self: *Self, + types: TypePair, + ) !Report { + var report = Report.init(self.gpa, "INVALID NOMINAL RECORD", .runtime_error); + errdefer report.deinit(); + + try report.document.addText("I'm having trouble with this nominal type that wraps a record:"); + try report.document.addLineBreak(); + + const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(types.actual_var))); + const region_info = self.module_env.calcRegionInfo(region.*); + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + const actual_type = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); + const expected_type = try report.addOwnedString(self.getFormattedString(types.expected_snapshot)); + + try report.document.addText("The record I found is:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_type); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("But the nominal type expects:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(expected_type); + + return report; + } + + /// Build a report for invalid nominal tuple (tuple elements don't match) + fn buildInvalidNominalTuple( + self: *Self, + types: TypePair, + ) !Report { + var report = Report.init(self.gpa, "INVALID NOMINAL TUPLE", .runtime_error); + errdefer report.deinit(); + + try report.document.addText("I'm having trouble with this nominal type that wraps a tuple:"); + try report.document.addLineBreak(); + + const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(types.actual_var))); + const region_info = self.module_env.calcRegionInfo(region.*); + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + const actual_type = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); + const expected_type = try report.addOwnedString(self.getFormattedString(types.expected_snapshot)); + + try report.document.addText("The tuple I found is:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_type); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("But the nominal type expects:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(expected_type); + + return report; + } + + /// Build a report for invalid nominal value (value type doesn't match) + fn buildInvalidNominalValue( + self: *Self, + types: TypePair, + ) !Report { + var report = Report.init(self.gpa, "INVALID NOMINAL TYPE", .runtime_error); + errdefer report.deinit(); + + try report.document.addText("I'm having trouble with this nominal type:"); + try report.document.addLineBreak(); + + const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(types.actual_var))); + const region_info = self.module_env.calcRegionInfo(region.*); + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + const actual_type = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); + const expected_type = try report.addOwnedString(self.getFormattedString(types.expected_snapshot)); + + try report.document.addText("The value I found has type:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_type); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("But the nominal type expects:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(expected_type); + + return report; + } + /// Build a report for function argument type mismatch fn buildIncompatibleFnCallArg( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, types: TypePair, data: IncompatibleFnCallArg, ) !Report { var report = Report.init(self.gpa, "TYPE MISMATCH", .runtime_error); errdefer report.deinit(); - self.buf.clearRetainingCapacity(); - try appendOrdinal(&self.buf, data.incompatible_arg_index + 1); - const arg_index = try report.addOwnedString(self.buf.items); + self.bytes_buf.clearRetainingCapacity(); + try appendOrdinal(&self.bytes_buf, data.incompatible_arg_index + 1); + const arg_index = try report.addOwnedString(self.bytes_buf.items); // Extract only the argument types from the function snapshots const actual_arg_type = self.extractFirstArgTypeFromFunctionSnapshot(types.actual_snapshot) orelse types.actual_snapshot; const expected_arg_type = self.extractFirstArgTypeFromFunctionSnapshot(types.expected_snapshot) orelse types.expected_snapshot; - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(actual_arg_type); - const actual_type = try report.addOwnedString(self.buf.items); - - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(expected_arg_type); - const expected_type = try report.addOwnedString(self.buf.items); + const actual_type = try report.addOwnedString(self.getFormattedString(actual_arg_type)); + const expected_type = try report.addOwnedString(self.getFormattedString(expected_arg_type)); try report.document.addText("The "); try report.document.addText(arg_index); @@ -1262,14 +1604,13 @@ pub const ReportBuilder = struct { try report.document.addReflowingText("This argument has the type:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(actual_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_type); try report.document.addLineBreak(); try report.document.addLineBreak(); try report.document.addReflowingText("But "); if (data.fn_name) |fn_name_ident| { - self.buf.clearRetainingCapacity(); const fn_name = try report.addOwnedString(self.can_ir.getIdent(fn_name_ident)); try report.document.addAnnotated(fn_name, .inline_code); } else { @@ -1279,38 +1620,32 @@ pub const ReportBuilder = struct { try report.document.addReflowingText(arg_index); try report.document.addReflowingText(" argument to be:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(expected_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(expected_type); return report; } fn buildIncompatibleFnArgsBoundVar( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, types: TypePair, data: IncompatibleFnArgsBoundVar, ) !Report { var report = Report.init(self.gpa, "TYPE MISMATCH", .runtime_error); errdefer report.deinit(); - self.buf.clearRetainingCapacity(); - try appendOrdinal(&self.buf, data.first_arg_index + 1); - const first_arg_index = try report.addOwnedString(self.buf.items); + self.bytes_buf.clearRetainingCapacity(); + try appendOrdinal(&self.bytes_buf, data.first_arg_index + 1); + const first_arg_index = try report.addOwnedString(self.bytes_buf.items); - self.buf.clearRetainingCapacity(); - try appendOrdinal(&self.buf, data.second_arg_index + 1); - const second_arg_index = try report.addOwnedString(self.buf.items); + self.bytes_buf.clearRetainingCapacity(); + try appendOrdinal(&self.bytes_buf, data.second_arg_index + 1); + const second_arg_index = try report.addOwnedString(self.bytes_buf.items); // The types from unification already have the correct snapshots // expected = first argument, actual = second argument - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const first_type = try report.addOwnedString(self.buf.items); - - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.actual_snapshot); - const second_type = try report.addOwnedString(self.buf.items); + const first_type = try report.addOwnedString(self.getFormattedString(types.expected_snapshot)); + const second_type = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); try report.document.addText("The "); try report.document.addText(first_arg_index); @@ -1382,8 +1717,8 @@ pub const ReportBuilder = struct { try report.document.addText(first_arg_index); try report.document.addReflowingText(" argument has the type:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(first_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(first_type); try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -1391,13 +1726,12 @@ pub const ReportBuilder = struct { try report.document.addText(second_arg_index); try report.document.addReflowingText(" argument has the type:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(second_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(second_type); try report.document.addLineBreak(); try report.document.addLineBreak(); if (data.fn_name) |fn_name_ident| { - self.buf.clearRetainingCapacity(); const fn_name = try report.addOwnedString(self.can_ir.getIdent(fn_name_ident)); try report.document.addAnnotated(fn_name, .inline_code); } else { @@ -1413,7 +1747,6 @@ pub const ReportBuilder = struct { /// Build a report for "number does not fit in type" diagnostic fn buildTypeApplyArityMismatchReport( self: *Self, - _: *snapshot.SnapshotWriter, data: TypeApplyArityMismatch, ) !Report { const title = blk: { @@ -1428,26 +1761,31 @@ pub const ReportBuilder = struct { var report = Report.init(self.gpa, title, .runtime_error); errdefer report.deinit(); - const type_name = try report.addOwnedString(self.can_ir.getIdent(data.type_name)); + // Look up display name in import mapping (handles auto-imported builtin types) + // If the type_name is in the mapping (e.g., "Builtin.Bool"), use the mapped display name ("Bool") + // Otherwise, use the identifier as-is + const type_name_ident = if (self.import_mapping.get(data.type_name)) |display_ident| + self.can_ir.getIdent(display_ident) + else + self.can_ir.getIdent(data.type_name); + const type_name = try report.addOwnedString(type_name_ident); - self.buf.clearRetainingCapacity(); - try self.buf.writer().print("{d}", .{data.num_expected_args}); - const num_expected_args = try report.addOwnedString(self.buf.items); + self.bytes_buf.clearRetainingCapacity(); + try self.bytes_buf.writer().print("{d}", .{data.num_expected_args}); + const num_expected_args = try report.addOwnedString(self.bytes_buf.items); - self.buf.clearRetainingCapacity(); - try self.buf.writer().print("{d}", .{data.num_actual_args}); - const num_actual_args = try report.addOwnedString(self.buf.items); - - const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.anno_var))); + self.bytes_buf.clearRetainingCapacity(); + try self.bytes_buf.writer().print("{d}", .{data.num_actual_args}); + const num_actual_args = try report.addOwnedString(self.bytes_buf.items); // Add source region highlighting - const region_info = self.module_env.calcRegionInfo(region.*); + const region_info = self.module_env.calcRegionInfo(data.region); try report.document.addReflowingText("The type "); try report.document.addAnnotated(type_name, .type_variable); try report.document.addReflowingText(" expects "); try report.document.addReflowingText(num_expected_args); - try report.document.addReflowingText(" argument, but got "); + try report.document.addReflowingText(pluralize(data.num_expected_args, " argument, but got ", " arguments, but got ")); try report.document.addReflowingText(num_actual_args); try report.document.addReflowingText(" instead."); try report.document.addLineBreak(); @@ -1459,25 +1797,643 @@ pub const ReportBuilder = struct { self.source, self.module_env.getLineStarts(), ); - try report.document.addLineBreak(); return report; } + /// Build a report for function call arity mismatch + fn buildFnCallArityMismatchReport( + self: *Self, + data: FnCallArityMismatch, + ) !Report { + const title = blk: { + if (data.expected_args > data.actual_args) { + break :blk "TOO FEW ARGUMENTS"; + } else if (data.expected_args < data.actual_args) { + break :blk "TOO MANY ARGUMENTS"; + } else { + break :blk "WRONG NUMBER OF ARGUMENTS"; + } + }; + var report = Report.init(self.gpa, title, .runtime_error); + errdefer report.deinit(); + + self.bytes_buf.clearRetainingCapacity(); + try self.bytes_buf.writer().print("{d}", .{data.expected_args}); + const num_expected = try report.addOwnedString(self.bytes_buf.items); + + self.bytes_buf.clearRetainingCapacity(); + try self.bytes_buf.writer().print("{d}", .{data.actual_args}); + const num_actual = try report.addOwnedString(self.bytes_buf.items); + + // Build the function type string from the snapshot + const fn_type = try report.addOwnedString(self.getFormattedString(data.fn_snapshot)); + + // Start the error message + if (data.fn_name) |fn_name_ident| { + const fn_name = try report.addOwnedString(self.can_ir.getIdent(fn_name_ident)); + try report.document.addReflowingText("The function "); + try report.document.addAnnotated(fn_name, .inline_code); + } else { + try report.document.addReflowingText("This function"); + } + try report.document.addReflowingText(" expects "); + try report.document.addReflowingText(num_expected); + try report.document.addReflowingText(pluralize(data.expected_args, " argument, but ", " arguments, but ")); + try report.document.addReflowingText(num_actual); + try report.document.addReflowingText(pluralize(data.actual_args, " was provided:", " were provided:")); + try report.document.addLineBreak(); + + // Add source region highlighting + const region_info = self.module_env.calcRegionInfo(data.call_region); + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + // Show the function signature + try report.document.addReflowingText("The function has the signature:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(fn_type); + + return report; + } + + /// Build a report for when a type alias references itself recursively + fn buildRecursiveAliasReport( + self: *Self, + data: RecursiveAlias, + ) !Report { + var report = Report.init(self.gpa, "RECURSIVE ALIAS", .runtime_error); + errdefer report.deinit(); + + // Look up display name in import mapping (handles auto-imported builtin types) + const type_name_ident = if (self.import_mapping.get(data.type_name)) |display_ident| + self.can_ir.getIdent(display_ident) + else + self.can_ir.getIdent(data.type_name); + const type_name = try report.addOwnedString(type_name_ident); + + // Add source region highlighting + const region_info = self.module_env.calcRegionInfo(data.region); + + try report.document.addReflowingText("The type alias "); + try report.document.addAnnotated(type_name, .type_variable); + try report.document.addReflowingText(" references itself, which is not allowed:"); + try report.document.addLineBreak(); + + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + try report.document.addReflowingText("Type aliases cannot be recursive. If you need a recursive type, use a nominal type (:=) instead of an alias (:)."); + + return report; + } + + /// Build a report for when alias syntax is used in a where clause + /// This syntax was used for abilities which have been removed + fn buildUnsupportedAliasWhereClauseReport( + self: *Self, + data: UnsupportedAliasWhereClause, + ) !Report { + var report = Report.init(self.gpa, "UNSUPPORTED WHERE CLAUSE", .runtime_error); + errdefer report.deinit(); + + const alias_name = try report.addOwnedString(self.can_ir.getIdent(data.alias_name)); + + // Add source region highlighting + const region_info = self.module_env.calcRegionInfo(data.region); + + try report.document.addReflowingText("The where clause syntax "); + try report.document.addAnnotated(alias_name, .type_variable); + try report.document.addReflowingText(" is not supported:"); + try report.document.addLineBreak(); + + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + try report.document.addReflowingText("This syntax was used for abilities, which have been removed from Roc. Use method constraints like `where [a.methodName(args) -> ret]` instead."); + + return report; + } + + // static dispatch // + + /// Build a report for when a type is not nominal, but you're trying to + /// static dispatch on it + fn buildStaticDispatchDispatcherNotNominal( + self: *Self, + data: DispatcherNotNominal, + ) !Report { + var report = Report.init(self.gpa, "MISSING METHOD", .runtime_error); + errdefer report.deinit(); + + const snapshot_str = try report.addOwnedString(self.getFormattedString(data.dispatcher_snapshot)); + + const method_name_str = try report.addOwnedString(self.can_ir.getIdentText(data.method_name)); + + const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.fn_var))); + + // Add source region highlighting + const region_info = self.module_env.calcRegionInfo(region.*); + + try report.document.addReflowingText("This "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(" method is being called on a value whose type doesn't have that method:"); + try report.document.addLineBreak(); + + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + try report.document.addReflowingText("The value's type, which does not have a method named "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(", is:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(snapshot_str); + + return report; + } + + /// Build a report for when a type doesn't have the expected static dispatch + /// method + fn buildStaticDispatchDispatcherDoesNotImplMethod( + self: *Self, + data: DispatcherDoesNotImplMethod, + ) !Report { + var report = Report.init(self.gpa, "MISSING METHOD", .runtime_error); + errdefer report.deinit(); + + const snapshot_str = try report.addOwnedString(self.getFormattedString(data.dispatcher_snapshot)); + + const method_name_str = try report.addOwnedString(self.can_ir.getIdentText(data.method_name)); + + const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.fn_var))); + + // Add source region highlighting + const region_info = self.module_env.calcRegionInfo(region.*); + + // Check if this method corresponds to an operator (using ident index comparison, not strings) + const is_from_binop = data.origin == .desugared_binop; + const mb_operator = self.getOperatorForMethod(data.method_name); + + if (is_from_binop) { + if (mb_operator) |operator| { + try report.document.addReflowingText("The value before this "); + try report.document.addAnnotated(operator, .emphasized); + try report.document.addReflowingText(" operator has a type that doesn't have a "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(" method:"); + } else { + try report.document.addReflowingText("This "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(" method is being called on a value whose type doesn't have that method:"); + } + try report.document.addLineBreak(); + } else { + try report.document.addReflowingText("This "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(" method is being called on a value whose type doesn't have that method:"); + try report.document.addLineBreak(); + } + + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + try report.document.addReflowingText("The value's type, which does not have a method named "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(", is:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(snapshot_str); + + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addAnnotated("Hint:", .emphasized); + switch (data.dispatcher_type) { + .nominal => { + if (is_from_binop) { + if (mb_operator) |operator| { + try report.document.addReflowingText("The "); + try report.document.addAnnotated(operator, .emphasized); + try report.document.addReflowingText(" operator calls a method named "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(" on the value preceding it, passing the value after the operator as the one argument."); + } else { + try report.document.addReflowingText(" For this to work, the type would need to have a method named "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(" associated with it in the type's declaration."); + } + } else { + try report.document.addReflowingText(" For this to work, the type would need to have a method named "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(" associated with it in the type's declaration."); + } + }, + .rigid => { + if (is_from_binop) { + if (mb_operator) |operator| { + try report.document.addReflowingText(" The "); + try report.document.addAnnotated(operator, .emphasized); + try report.document.addReflowingText(" operator requires the type to have a "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(" method. Did you forget to specify it in the type annotation?"); + } else { + try report.document.addReflowingText(" Did you forget to specify "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(" in the type annotation?"); + } + } else { + try report.document.addReflowingText(" Did you forget to specify "); + try report.document.addAnnotated(method_name_str, .emphasized); + try report.document.addReflowingText(" in the type annotation?"); + } + }, + } + + return report; + } + + /// Build a report for when an anonymous type doesn't support equality + fn buildTypeDoesNotSupportEquality( + self: *Self, + data: TypeDoesNotSupportEquality, + ) !Report { + var report = Report.init(self.gpa, "TYPE DOES NOT SUPPORT EQUALITY", .runtime_error); + errdefer report.deinit(); + + const snapshot_str = try report.addOwnedString(self.getFormattedString(data.dispatcher_snapshot)); + + const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.fn_var))); + const region_info = self.module_env.calcRegionInfo(region.*); + + try report.document.addReflowingText("This expression uses "); + try report.document.addAnnotated("==", .emphasized); + try report.document.addReflowingText(" or "); + try report.document.addAnnotated("!=", .emphasized); + try report.document.addReflowingText(" on a type that doesn't support equality:"); + try report.document.addLineBreak(); + + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + try report.document.addReflowingText("The type is:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(snapshot_str); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + // Get the content and explain which parts don't support equality + const content = self.snapshots.getContent(data.dispatcher_snapshot); + if (content == .structure) { + switch (content.structure) { + .record => |record| { + try self.explainRecordEqualityFailure(&report, record); + }, + .tuple => |tuple| { + try self.explainTupleEqualityFailure(&report, tuple); + }, + .tag_union => |tag_union| { + try self.explainTagUnionEqualityFailure(&report, tag_union); + }, + .fn_pure, .fn_effectful, .fn_unbound => { + try report.document.addReflowingText("Functions cannot be compared for equality."); + try report.document.addLineBreak(); + }, + else => {}, + } + } + + return report; + } + + /// Build a report for when an anonymous type doesn't support equality + fn buildCannotAccessOpaqueNominal( + self: *Self, + data: CannotAccessOpaqueNominal, + ) !Report { + var report = Report.init(self.gpa, "CANNOT USE OPAQUE NOMINAL TYPE", .runtime_error); + errdefer report.deinit(); + + const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.var_))); + const region_info = self.module_env.calcRegionInfo(region.*); + + try report.document.addReflowingText("You're attempting to create an instance of "); + try report.document.addAnnotated(self.can_ir.getIdentText(data.nominal_type_name), .inline_code); + try report.document.addReflowingText(", but it's an "); + try report.document.addAnnotated("opaque", .emphasized); + try report.document.addReflowingText(" type:"); + try report.document.addLineBreak(); + + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + + try report.document.addLineBreak(); + try report.document.addAnnotated("Hint:", .emphasized); + try report.document.addReflowingText(" To create an instance of this type outside the module it's defined it, you have to define it with "); + try report.document.addAnnotated(":=", .emphasized); + try report.document.addReflowingText(" instead of "); + try report.document.addAnnotated("::", .emphasized); + try report.document.addReflowingText("."); + + return report; + } + + /// Build a report for when a nominal type variable doesn't resolve properly. + /// This is a compiler bug - it should never happen in correctly compiled code. + fn buildNominalTypeResolutionFailed( + self: *Self, + data: NominalTypeResolutionFailed, + ) !Report { + var report = Report.init(self.gpa, "COMPILER BUG", .runtime_error); + errdefer report.deinit(); + + const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.var_))); + const region_info = self.module_env.calcRegionInfo(region.*); + + try report.document.addReflowingText("An internal compiler error occurred while checking this nominal type usage:"); + try report.document.addLineBreak(); + + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + + try report.document.addLineBreak(); + try report.document.addReflowingText("The nominal type declaration variable did not resolve to a nominal type structure. This indicates a bug in the Roc compiler. Please report this issue at https://github.com/roc-lang/roc/issues"); + + return report; + } + + /// Explain which record fields don't support equality + fn explainRecordEqualityFailure( + self: *Self, + report: *Report, + record: snapshot.SnapshotRecord, + ) !void { + const fields = self.snapshots.sliceRecordFields(record.fields); + var has_problem_fields = false; + + // First pass: check if any fields don't support equality + for (fields.items(.content)) |field_content_idx| { + if (!self.snapshotSupportsEquality(field_content_idx)) { + has_problem_fields = true; + break; + } + } + + if (has_problem_fields) { + try report.document.addReflowingText("This record does not support equality because these fields have types that don't support "); + try report.document.addAnnotated("is_eq", .emphasized); + try report.document.addReflowingText(":"); + try report.document.addLineBreak(); + + const field_names = fields.items(.name); + const field_contents = fields.items(.content); + for (field_names, field_contents) |name, field_content_idx| { + if (!self.snapshotSupportsEquality(field_content_idx)) { + const field_name = self.can_ir.getIdentText(name); + + const field_type_str = try report.addOwnedString(self.getFormattedString(field_content_idx)); + + try report.document.addText(" "); + try report.document.addAnnotated(field_name, .emphasized); + try report.document.addText(": "); + try report.document.addAnnotated(field_type_str, .type_variable); + try report.document.addLineBreak(); + } + } + try report.document.addLineBreak(); + try report.document.addAnnotated("Hint:", .emphasized); + try report.document.addReflowingText(" Anonymous records only have an "); + try report.document.addAnnotated("is_eq", .emphasized); + try report.document.addReflowingText(" method if all of their fields have "); + try report.document.addAnnotated("is_eq", .emphasized); + try report.document.addReflowingText(" methods."); + try report.document.addLineBreak(); + } + } + + /// Explain which tuple elements don't support equality + fn explainTupleEqualityFailure( + self: *Self, + report: *Report, + tuple: snapshot.SnapshotTuple, + ) !void { + const elems = self.snapshots.sliceVars(tuple.elems); + var has_problem_elems = false; + + // First pass: check if any elements don't support equality + for (elems) |elem_content_idx| { + if (!self.snapshotSupportsEquality(elem_content_idx)) { + has_problem_elems = true; + break; + } + } + + if (has_problem_elems) { + try report.document.addReflowingText("This tuple does not support equality because these elements have types that don't support "); + try report.document.addAnnotated("is_eq", .emphasized); + try report.document.addReflowingText(":"); + try report.document.addLineBreak(); + + for (elems, 0..) |elem_content_idx, i| { + if (!self.snapshotSupportsEquality(elem_content_idx)) { + const elem_type_str = try report.addOwnedString(self.getFormattedString(elem_content_idx)); + + try report.document.addText(" element "); + var buf: [20]u8 = undefined; + const index_str = std.fmt.bufPrint(&buf, "{}", .{i}) catch "?"; + try report.document.addAnnotated(index_str, .emphasized); + try report.document.addText(": "); + try report.document.addAnnotated(elem_type_str, .type_variable); + try report.document.addLineBreak(); + } + } + try report.document.addLineBreak(); + try report.document.addAnnotated("Hint:", .emphasized); + try report.document.addReflowingText(" Tuples only have an "); + try report.document.addAnnotated("is_eq", .emphasized); + try report.document.addReflowingText(" method if all of their elements have "); + try report.document.addAnnotated("is_eq", .emphasized); + try report.document.addReflowingText(" methods."); + try report.document.addLineBreak(); + } + } + + /// Explain which tag union payloads don't support equality + fn explainTagUnionEqualityFailure( + self: *Self, + report: *Report, + tag_union: snapshot.SnapshotTagUnion, + ) !void { + const tags = self.snapshots.sliceTags(tag_union.tags); + var has_problem_tags = false; + + // First pass: check if any tag payloads don't support equality + for (tags.items(.args)) |tag_args| { + const args = self.snapshots.sliceVars(tag_args); + for (args) |arg_content_idx| { + if (!self.snapshotSupportsEquality(arg_content_idx)) { + has_problem_tags = true; + break; + } + } + if (has_problem_tags) break; + } + + if (has_problem_tags) { + try report.document.addReflowingText("This tag union does not support equality because these tags have payload types that don't support "); + try report.document.addAnnotated("is_eq", .emphasized); + try report.document.addReflowingText(":"); + try report.document.addLineBreak(); + + const tag_names = tags.items(.name); + const tag_args_list = tags.items(.args); + for (tag_names, tag_args_list) |name, tag_args| { + const args = self.snapshots.sliceVars(tag_args); + var tag_has_problem = false; + for (args) |arg_content_idx| { + if (!self.snapshotSupportsEquality(arg_content_idx)) { + tag_has_problem = true; + break; + } + } + if (tag_has_problem) { + const tag_name = self.can_ir.getIdentText(name); + try report.document.addText(" "); + try report.document.addAnnotated(tag_name, .emphasized); + + // Show the problematic payload types + if (args.len > 0) { + try report.document.addText(" ("); + var first = true; + for (args) |arg_content_idx| { + if (!first) try report.document.addText(", "); + first = false; + + const arg_type_str = try report.addOwnedString(self.getFormattedString(arg_content_idx)); + try report.document.addAnnotated(arg_type_str, .type_variable); + } + try report.document.addText(")"); + } + try report.document.addLineBreak(); + } + } + try report.document.addLineBreak(); + try report.document.addAnnotated("Hint:", .emphasized); + try report.document.addReflowingText(" Tag unions only have an "); + try report.document.addAnnotated("is_eq", .emphasized); + try report.document.addReflowingText(" method if all of their payload types have "); + try report.document.addAnnotated("is_eq", .emphasized); + try report.document.addReflowingText(" methods."); + try report.document.addLineBreak(); + } + } + + /// Check if a snapshotted type supports equality + fn snapshotSupportsEquality(self: *Self, content_idx: snapshot.SnapshotContentIdx) bool { + const content = self.snapshots.getContent(content_idx); + return switch (content) { + .structure => |s| switch (s) { + // Functions never support equality + .fn_pure, .fn_effectful, .fn_unbound => false, + // Empty types trivially support equality + .empty_record, .empty_tag_union => true, + // Records: all fields must support equality + .record => |record| { + const fields = self.snapshots.sliceRecordFields(record.fields); + for (fields.items(.content)) |field_content| { + if (!self.snapshotSupportsEquality(field_content)) return false; + } + return true; + }, + // Tuples: all elements must support equality + .tuple => |tuple| { + const elems = self.snapshots.sliceVars(tuple.elems); + for (elems) |elem_content| { + if (!self.snapshotSupportsEquality(elem_content)) return false; + } + return true; + }, + // Tag unions: all payloads must support equality + .tag_union => |tag_union| { + const tags_slice = self.snapshots.sliceTags(tag_union.tags); + for (tags_slice.items(.args)) |tag_args| { + const args = self.snapshots.sliceVars(tag_args); + for (args) |arg_content| { + if (!self.snapshotSupportsEquality(arg_content)) return false; + } + } + return true; + }, + // Other types (nominal, box, etc.) assumed to support equality + else => true, + }, + // Aliases: check the underlying type + .alias => |alias| self.snapshotSupportsEquality(alias.backing), + // Recursion vars: assume they support equality + .recursion_var => true, + // Other types (flex, rigid, recursive, err) assumed to support equality + else => true, + }; + } + // number problems // /// Build a report for "number does not fit in type" diagnostic fn buildNumberDoesNotFitReport( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, data: NumberDoesNotFit, ) !Report { var report = Report.init(self.gpa, "NUMBER DOES NOT FIT IN TYPE", .runtime_error); errdefer report.deinit(); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(data.expected_type); - const owned_expected = try report.addOwnedString(self.buf.items[0..]); + const owned_expected = try report.addOwnedString(self.getFormattedString(data.expected_type)); const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.literal_var))); @@ -1501,8 +2457,8 @@ pub const ReportBuilder = struct { try report.document.addText("Its inferred type is:"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(owned_expected, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(owned_expected); return report; } @@ -1510,15 +2466,12 @@ pub const ReportBuilder = struct { /// Build a report for "negative unsigned integer" diagnostic fn buildNegativeUnsignedIntReport( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, data: NegativeUnsignedInt, ) !Report { var report = Report.init(self.gpa, "NEGATIVE UNSIGNED INTEGER", .runtime_error); errdefer report.deinit(); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(data.expected_type); - const owned_expected = try report.addOwnedString(self.buf.items[0..]); + const owned_expected = try report.addOwnedString(self.getFormattedString(data.expected_type)); const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.literal_var))); @@ -1546,8 +2499,107 @@ pub const ReportBuilder = struct { try report.document.addAnnotated("unsigned", .emphasized); try report.document.addReflowingText(":"); try report.document.addLineBreak(); - try report.document.addText(" "); - try report.document.addAnnotated(owned_expected, .type_variable); + try report.document.addLineBreak(); + try report.document.addCodeBlock(owned_expected); + + return report; + } + + /// Build a report for "invalid numeric literal" diagnostic + fn buildInvalidNumericLiteralReport( + self: *Self, + data: InvalidNumericLiteral, + ) !Report { + var report = Report.init(self.gpa, "INVALID NUMERIC LITERAL", .runtime_error); + errdefer report.deinit(); + + const owned_expected = try report.addOwnedString(self.getFormattedString(data.expected_type)); + + // Add source region highlighting + const region_info = self.module_env.calcRegionInfo(data.region); + const literal_text = self.source[data.region.start.offset..data.region.end.offset]; + + if (data.is_fractional) { + // Fractional literal to integer type + try report.document.addReflowingText("The fractional literal "); + try report.document.addAnnotated(literal_text, .emphasized); + try report.document.addReflowingText(" cannot be converted to an integer type:"); + try report.document.addLineBreak(); + + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + try report.document.addText("Its inferred type is:"); + try report.document.addLineBreak(); + try report.document.addCodeBlock(owned_expected); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addReflowingText("Hint: Use a decimal type like "); + try report.document.addAnnotated("Dec", .type_variable); + try report.document.addReflowingText(" or "); + try report.document.addAnnotated("F64", .type_variable); + try report.document.addReflowingText(" instead."); + } else { + // Integer literal out of range + try report.document.addReflowingText("The numeric literal "); + try report.document.addAnnotated(literal_text, .emphasized); + try report.document.addReflowingText(" is out of range for its inferred type:"); + try report.document.addLineBreak(); + + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + try report.document.addText("Its inferred type is:"); + try report.document.addLineBreak(); + try report.document.addCodeBlock(owned_expected); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addReflowingText("Hint: Use a larger integer type or "); + try report.document.addAnnotated("Dec", .type_variable); + try report.document.addReflowingText(" for arbitrary precision."); + } + + return report; + } + + /// Build a report for "unused value" diagnostic + fn buildUnusedValueReport(self: *Self, data: UnusedValue) !Report { + var report = Report.init(self.gpa, "UNUSED VALUE", .runtime_error); + errdefer report.deinit(); + + const owned_expected = try report.addOwnedString(self.getFormattedString(data.snapshot)); + + const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.var_))); + const region_info = self.module_env.calcRegionInfo(region.*); + + try report.document.addReflowingText("This expression produces a value, but it's not being used:"); + try report.document.addLineBreak(); + + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + try report.document.addReflowingText("It has the type:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(owned_expected); return report; } @@ -1557,7 +2609,6 @@ pub const ReportBuilder = struct { /// Build a report for cross-module import type mismatch fn buildCrossModuleImportError( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, types: TypePair, data: CrossModuleImport, ) !Report { @@ -1617,11 +2668,9 @@ pub const ReportBuilder = struct { try report.document.addText("It has the type:"); try report.document.addLineBreak(); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const expected_type = try report.addOwnedString(self.buf.items); - try report.document.addText(" "); - try report.document.addAnnotated(expected_type, .type_variable); + const expected_type = try report.addOwnedString(self.getFormattedString(types.expected_snapshot)); + try report.document.addLineBreak(); + try report.document.addCodeBlock(expected_type); try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -1635,19 +2684,164 @@ pub const ReportBuilder = struct { } try report.document.addLineBreak(); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.actual_snapshot); - const actual_type = try report.addOwnedString(self.buf.items); - try report.document.addText(" "); - try report.document.addAnnotated(actual_type, .type_variable); + const actual_type = try report.addOwnedString(self.getFormattedString(types.actual_snapshot)); try report.document.addLineBreak(); + try report.document.addCodeBlock(actual_type); return report; } /// Build a report for "invalid number literal" diagnostic - fn buildUnimplementedReport(self: *Self) !Report { - const report = Report.init(self.gpa, "UNIMPLEMENTED", .runtime_error); + fn buildUnimplementedReport(self: *Self, bytes: []const u8) !Report { + var report = Report.init(self.gpa, "UNIMPLEMENTED: ", .runtime_error); + const owned_bytes = try report.addOwnedString(bytes); + try report.document.addText(owned_bytes); + return report; + } + + fn buildPlatformAliasNotFound(self: *Self, data: PlatformAliasNotFound) !Report { + var report = Report.init(self.gpa, "PLATFORM EXPECTED ALIAS: ", .runtime_error); + errdefer report.deinit(); + + const owned_name = try report.addOwnedString(self.can_ir.getIdentText(data.expected_alias_ident)); + + try report.document.addReflowingText("The platform expected your "); + try report.document.addAnnotated("app", .inline_code); + try report.document.addReflowingText(" module to have a type alias named:"); + try report.document.addLineBreak(); + try report.document.addCodeBlock(owned_name); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addReflowingText("But I could not find it."); + + switch (data.ctx) { + .not_found => {}, + .found_but_not_alias => { + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addAnnotated("Hint:", .emphasized); + try report.document.addReflowingText(" You have a type with the name "); + try report.document.addAnnotated(owned_name, .type_variable); + try report.document.addReflowingText(", but it's not an alias."); + }, + } + + return report; + } + + fn buildPlatformDefNotFound(self: *Self, data: PlatformDefNotFound) !Report { + var report = Report.init(self.gpa, "PLATFORM EXPECTED DEFINITION: ", .runtime_error); + errdefer report.deinit(); + + const owned_name = try report.addOwnedString(self.can_ir.getIdentText(data.expected_def_ident)); + + try report.document.addReflowingText("The platform expected your "); + try report.document.addAnnotated("app", .inline_code); + try report.document.addReflowingText(" module to have a exported definition named:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(owned_name); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addReflowingText("But I could not find it."); + + switch (data.ctx) { + .not_found => {}, + .found_but_not_exported => { + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addAnnotated("Hint:", .emphasized); + try report.document.addReflowingText(" You have a definition with the name "); + try report.document.addAnnotated(owned_name, .type_variable); + try report.document.addReflowingText(", but it's not exported. Maybe add it to the export list?"); + }, + } + + return report; + } + + /// Build a report for compile-time crash + fn buildComptimeCrashReport(self: *Self, data: ComptimeCrash) !Report { + var report = Report.init(self.gpa, "COMPTIME CRASH", .runtime_error); + errdefer report.deinit(); + + const owned_message = try report.addOwnedString(data.message); + + try report.document.addText("This definition crashed during compile-time evaluation:"); + try report.document.addLineBreak(); + + // Add source region highlighting + const region_info = self.module_env.calcRegionInfo(data.region); + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + try report.document.addText("The "); + try report.document.addAnnotated("crash", .keyword); + try report.document.addText(" happened with this message:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(owned_message); + + return report; + } + + /// Build a report for compile-time expect failure + fn buildComptimeExpectFailedReport(self: *Self, data: ComptimeExpectFailed) !Report { + // Note: data.message contains raw source bytes which we don't display separately + // since the source region highlighting already shows the expect expression + var report = Report.init(self.gpa, "COMPTIME EXPECT FAILED", .runtime_error); + errdefer report.deinit(); + + try report.document.addText("This "); + try report.document.addAnnotated("expect", .keyword); + try report.document.addText(" failed during compile-time evaluation:"); + try report.document.addLineBreak(); + + // Add source region highlighting - shows the expect expression with syntax highlighting + const region_info = self.module_env.calcRegionInfo(data.region); + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + + return report; + } + + /// Build a report for compile-time evaluation error + fn buildComptimeEvalErrorReport(self: *Self, data: ComptimeEvalError) !Report { + var report = Report.init(self.gpa, "COMPTIME EVAL ERROR", .runtime_error); + errdefer report.deinit(); + + const owned_error_name = try report.addOwnedString(data.error_name); + + try report.document.addText("This definition could not be evaluated at compile time:"); + try report.document.addLineBreak(); + + // Add source region highlighting + const region_info = self.module_env.calcRegionInfo(data.region); + try report.document.addSourceRegion( + region_info, + .error_highlight, + self.filename, + self.source, + self.module_env.getLineStarts(), + ); + try report.document.addLineBreak(); + + try report.document.addText("The evaluation failed with error:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addCodeBlock(owned_error_name); + return report; } @@ -1655,7 +2849,7 @@ pub const ReportBuilder = struct { // Given a buffer and a number, write a the human-readably ordinal number // Note that the caller likely needs to clear the buffer before calling this function - fn appendOrdinal(buf: *std.ArrayList(u8), n: u32) !void { + fn appendOrdinal(buf: *std.array_list.Managed(u8), n: u32) !void { switch (n) { 1 => try buf.appendSlice("first"), 2 => try buf.appendSlice("second"), @@ -1684,13 +2878,12 @@ pub const ReportBuilder = struct { } /// Check if both snapshot contents represent function types - fn areBothFunctionSnapshots(self: *Self, expected_content: snapshot.SnapshotContent, actual_content: snapshot.SnapshotContent) bool { - return self.isSnapshotFunction(expected_content) and self.isSnapshotFunction(actual_content); + fn areBothFunctionSnapshots(expected_content: snapshot.SnapshotContent, actual_content: snapshot.SnapshotContent) bool { + return isSnapshotFunction(expected_content) and isSnapshotFunction(actual_content); } /// Check if a snapshot content represents a function type - fn isSnapshotFunction(self: *Self, content: snapshot.SnapshotContent) bool { - _ = self; + fn isSnapshotFunction(content: snapshot.SnapshotContent) bool { return switch (content) { .structure => |structure| switch (structure) { .fn_pure, .fn_effectful, .fn_unbound => true, @@ -1729,16 +2922,16 @@ pub const ReportBuilder = struct { /// looses essential error information. So before doing this, we create a fully /// resolved snapshot of the type that we can use in reporting /// -/// Entry points are `appendProblem` and `deepCopyVar` +/// Entry point is `appendProblem` pub const Store = struct { const Self = @This(); - const ALIGNMENT = 16; + const ALIGNMENT = std.mem.Alignment.@"16"; - problems: std.ArrayListAlignedUnmanaged(Problem, ALIGNMENT) = .{}, + problems: std.ArrayListAligned(Problem, ALIGNMENT) = .{}, pub fn initCapacity(gpa: Allocator, capacity: usize) std.mem.Allocator.Error!Self { return .{ - .problems = try std.ArrayListAlignedUnmanaged(Problem, ALIGNMENT).initCapacity(gpa, capacity), + .problems = try std.ArrayListAligned(Problem, ALIGNMENT).initCapacity(gpa, capacity), }; } diff --git a/src/check/snapshot.zig b/src/check/snapshot.zig index 1585311064..e7cc57d065 100644 --- a/src/check/snapshot.zig +++ b/src/check/snapshot.zig @@ -18,10 +18,132 @@ const SnapshotContentList = collections.SafeList(SnapshotContent); const SnapshotContentIdxSafeList = collections.SafeList(SnapshotContentIdx); const SnapshotRecordFieldSafeList = collections.SafeMultiList(SnapshotRecordField); const SnapshotTagSafeList = collections.SafeMultiList(SnapshotTag); +const SnapshotStaticDispatchConstraintSafeList = collections.SafeList(SnapshotStaticDispatchConstraint); const MkSafeMultiList = collections.SafeMultiList; +/// The content of a type snapshot, mirroring types.Content for error reporting. +pub const SnapshotContent = union(enum) { + flex: SnapshotFlex, + rigid: SnapshotRigid, + alias: SnapshotAlias, + structure: SnapshotFlatType, + recursion_var: SnapshotRecursionVar, + /// A recursive type reference. Stores the name of the type variable if available. + recursive: ?Ident.Idx, + err, +}; + +/// A snapshotted recursion variable that points to its recursive structure. +pub const SnapshotRecursionVar = struct { + structure: SnapshotContentIdx, + name: ?base.Ident.Idx, +}; + +/// A snapshotted flex (unbound) type variable with optional name and constraints. +pub const SnapshotFlex = struct { + name: ?Ident.Idx, + var_: Var, + constraints: SnapshotStaticDispatchConstraintSafeList.Range, +}; + +/// A snapshotted rigid (bound) type variable with name and constraints. +pub const SnapshotRigid = struct { + name: Ident.Idx, + constraints: SnapshotStaticDispatchConstraintSafeList.Range, +}; + +/// A snapshotted type alias with its backing type and type variables. +pub const SnapshotAlias = struct { + ident: types.TypeIdent, + backing: SnapshotContentIdx, + vars: SnapshotContentIdxSafeList.Range, +}; + +/// A snapshotted flat type structure (non-variable types like records, functions, etc). +pub const SnapshotFlatType = union(enum) { + box: SnapshotContentIdx, + tuple: SnapshotTuple, + nominal_type: SnapshotNominalType, + fn_pure: SnapshotFunc, + fn_effectful: SnapshotFunc, + fn_unbound: SnapshotFunc, + record: SnapshotRecord, + record_unbound: SnapshotRecordFieldSafeList.Range, + empty_record, + tag_union: SnapshotTagUnion, + empty_tag_union, +}; + +/// A snapshotted tuple type with its element types. +pub const SnapshotTuple = struct { + elems: SnapshotContentIdxSafeList.Range, +}; + +/// A snapshotted nominal (named) type with its type parameters and origin module. +pub const SnapshotNominalType = struct { + ident: types.TypeIdent, + vars: SnapshotContentIdxSafeList.Range, + origin_module: Ident.Idx, +}; + +/// A snapshotted function type with argument types, return type, and instantiation flag. +pub const SnapshotFunc = struct { + args: SnapshotContentIdxSafeList.Range, + ret: SnapshotContentIdx, + needs_instantiation: bool, +}; + +/// A snapshotted record type with fields and extension variable. +pub const SnapshotRecord = struct { + fields: SnapshotRecordFieldSafeList.Range, + ext: SnapshotContentIdx, +}; + +/// A single field in a snapshotted record type. +pub const SnapshotRecordField = struct { + name: Ident.Idx, + content: SnapshotContentIdx, + + const Self = @This(); + + /// Returns true if field `a` should sort before field `b` by name. + pub fn sortByNameAsc(ident_store: *const Ident.Store, a: Self, b: Self) bool { + return Self.orderByName(ident_store, a, b) == .lt; + } + + /// Compares two record fields by their name for ordering. + pub fn orderByName(store: *const Ident.Store, a: Self, b: Self) std.math.Order { + const a_text = store.getText(a.name); + const b_text = store.getText(b.name); + return std.mem.order(u8, a_text, b_text); + } +}; + +/// A snapshotted tag union type with its tags and extension variable. +pub const SnapshotTagUnion = struct { + tags: SnapshotTagSafeList.Range, + ext: SnapshotContentIdx, +}; + +/// A single tag in a snapshotted tag union with its name and argument types. +pub const SnapshotTag = struct { + name: Ident.Idx, + args: SnapshotContentIdxSafeList.Range, +}; + +/// A snapshotted static dispatch constraint for method resolution. +pub const SnapshotStaticDispatchConstraint = struct { + fn_name: Ident.Idx, + fn_content: SnapshotContentIdx, + /// The type variable that has this constraint (the dispatcher). + /// This is the type that the method is called on. + dispatcher: SnapshotContentIdx, +}; + const Var = types.Var; const Content = types.Content; +const Flex = types.Flex; +const Rigid = types.Rigid; /// Self-contained snapshot store with fully resolved content (ie no Vars) /// @@ -30,7 +152,12 @@ const Content = types.Content; /// looses essential error information. So before doing this, we create a fully /// resolved snapshot of the type that we can use in reporting /// -/// Entry point is `deepCopyVar` +/// Entry point is `snapshotVarForError` +const TypeWriter = types.TypeWriter; + +/// Stores snapshots of types captured before unification errors overwrite them with `.err`. +/// This allows error messages to display the original conflicting types rather than the +/// error state. Also stores pre-formatted type strings for efficient error reporting. pub const Store = struct { const Self = @This(); @@ -39,107 +166,212 @@ pub const Store = struct { // Content storage contents: SnapshotContentList, + // Catch recursive references + seen_vars: base.Scratch(Var), + /// Storage for compound type parts content_indexes: SnapshotContentIdxSafeList, record_fields: SnapshotRecordFieldSafeList, tags: SnapshotTagSafeList, + static_dispatch_constraints: SnapshotStaticDispatchConstraintSafeList, // Scratch scratch_content: base.Scratch(SnapshotContentIdx), scratch_tags: base.Scratch(SnapshotTag), scratch_record_fields: base.Scratch(SnapshotRecordField), + scratch_static_dispatch_constraints: base.Scratch(SnapshotStaticDispatchConstraint), + + /// Formatted type strings, indexed by SnapshotContentIdx + formatted_strings: std.AutoHashMapUnmanaged(SnapshotContentIdx, []const u8), pub fn initCapacity(gpa: Allocator, capacity: usize) std.mem.Allocator.Error!Self { return .{ .gpa = gpa, .contents = try SnapshotContentList.initCapacity(gpa, capacity), + .seen_vars = try base.Scratch(Var).init(gpa), .content_indexes = try SnapshotContentIdxSafeList.initCapacity(gpa, capacity), .record_fields = try SnapshotRecordFieldSafeList.initCapacity(gpa, 256), .tags = try SnapshotTagSafeList.initCapacity(gpa, 256), + .static_dispatch_constraints = try SnapshotStaticDispatchConstraintSafeList.initCapacity(gpa, 64), .scratch_content = try base.Scratch(SnapshotContentIdx).init(gpa), .scratch_tags = try base.Scratch(SnapshotTag).init(gpa), .scratch_record_fields = try base.Scratch(SnapshotRecordField).init(gpa), + .scratch_static_dispatch_constraints = try base.Scratch(SnapshotStaticDispatchConstraint).init(gpa), + .formatted_strings = .{}, }; } pub fn deinit(self: *Self) void { + // Free all stored formatted strings + var iter = self.formatted_strings.valueIterator(); + while (iter.next()) |str| { + self.gpa.free(str.*); + } + self.formatted_strings.deinit(self.gpa); + self.contents.deinit(self.gpa); + self.seen_vars.deinit(); self.content_indexes.deinit(self.gpa); self.record_fields.deinit(self.gpa); self.tags.deinit(self.gpa); - self.scratch_content.deinit(self.gpa); - self.scratch_tags.deinit(self.gpa); - self.scratch_record_fields.deinit(self.gpa); + self.static_dispatch_constraints.deinit(self.gpa); + self.scratch_content.deinit(); + self.scratch_tags.deinit(); + self.scratch_record_fields.deinit(); + self.scratch_static_dispatch_constraints.deinit(); } - /// Create a deep snapshot from a Var, storing it in this SnapshotStore - /// Deep copy a type variable's content into self-contained snapshot storage - pub fn deepCopyVar(self: *Self, store: *const TypesStore, var_: types.Var) std.mem.Allocator.Error!SnapshotContentIdx { + /// Get the pre-formatted string for a snapshot. + pub fn getFormattedString(self: *const Self, idx: SnapshotContentIdx) ?[]const u8 { + return self.formatted_strings.get(idx); + } + + /// Deep copy a type variable for error reporting. This snapshots the type structure + /// AND formats each nested type using TypeWriter before the types get overwritten with .err. + /// ONLY use this in error paths - it allocates formatted strings for all nested types. + pub fn snapshotVarForError(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, var_: types.Var) std.mem.Allocator.Error!SnapshotContentIdx { + const snapshot_idx = try self.deepCopyVarInternal(store, type_writer, var_); + return snapshot_idx; + } + + /// Internal recursive implementation of snapshotVarForError + fn deepCopyVarInternal(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, var_: types.Var) std.mem.Allocator.Error!SnapshotContentIdx { const resolved = store.resolveVar(var_); - return try self.deepCopyContent(store, resolved.desc.content); + + // Check if we've seen this variable + var has_seen_var = false; + for (self.seen_vars.items.items) |seen_var| { + if (seen_var == resolved.var_) { + has_seen_var = true; + break; + } + } + + // If we've seen this variable, then return it as a recursive type + // Try to extract the name from the content for better error messages + if (has_seen_var) { + const recursive_name: ?Ident.Idx = switch (resolved.desc.content) { + .flex => |flex| flex.name, + .rigid => |rigid| rigid.name, + .recursion_var => |rec_var| rec_var.name, + .alias => |alias| alias.ident.ident_idx, + .structure => |flat_type| switch (flat_type) { + .nominal_type => |nominal| nominal.ident.ident_idx, + // Other structures can appear as backing vars for nominal types. + // E.g., List(a) := [Nil, Cons(a, List(a))] has a tag union as backing. + // These don't have a direct name, so we fall back to contextual naming. + .record, .record_unbound, .tuple, .fn_pure, .fn_effectful, .fn_unbound, .empty_record, .tag_union, .empty_tag_union => null, + }, + // Error types shouldn't create cycles + .err => unreachable, + }; + return try self.contents.append(self.gpa, .{ .recursive = recursive_name }); + } + + // If not, add it to the seen list + try self.seen_vars.append(resolved.var_); + defer _ = self.seen_vars.pop(); + + const snapshot_idx = try self.deepCopyContent(store, type_writer, resolved.var_, resolved.desc.content); + + // Format this type and store the formatted string + type_writer.reset(); + try type_writer.write(var_, .wrap); + const formatted = try self.gpa.dupe(u8, type_writer.get()); + try self.formatted_strings.put(self.gpa, snapshot_idx, formatted); + + return snapshot_idx; } - fn deepCopyContent(self: *Self, store: *const TypesStore, content: Content) std.mem.Allocator.Error!SnapshotContentIdx { + fn deepCopyFlex(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, var_: types.Var, flex: types.Flex) std.mem.Allocator.Error!SnapshotFlex { + return SnapshotFlex{ + .name = flex.name, + .var_ = var_, + .constraints = try self.deepCopyStaticDispatchConstraintRange(store, type_writer, flex.constraints), + }; + } + + fn deepCopyRigid(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, rigid: types.Rigid) std.mem.Allocator.Error!SnapshotRigid { + return SnapshotRigid{ + .name = rigid.name, + .constraints = try self.deepCopyStaticDispatchConstraintRange(store, type_writer, rigid.constraints), + }; + } + + fn deepCopyStaticDispatchConstraintRange( + self: *Self, + store: *const TypesStore, + type_writer: *TypeWriter, + range: types.StaticDispatchConstraint.SafeList.Range, + ) std.mem.Allocator.Error!SnapshotStaticDispatchConstraintSafeList.Range { + const scratch_top = self.scratch_static_dispatch_constraints.top(); + defer self.scratch_static_dispatch_constraints.clearFrom(scratch_top); + + for (store.sliceStaticDispatchConstraints(range)) |constraint| { + try self.scratch_static_dispatch_constraints.append(try self.deepCopyStaticDispatchConstraint(store, type_writer, constraint)); + } + + return self.static_dispatch_constraints.appendSlice(self.gpa, self.scratch_static_dispatch_constraints.sliceFromStart(scratch_top)); + } + + fn deepCopyStaticDispatchConstraint( + self: *Self, + store: *const TypesStore, + type_writer: *TypeWriter, + constraint: types.StaticDispatchConstraint, + ) std.mem.Allocator.Error!SnapshotStaticDispatchConstraint { + return SnapshotStaticDispatchConstraint{ + .fn_name = constraint.fn_name, + .fn_content = try self.deepCopyVarInternal(store, type_writer, constraint.fn_var), + // Dispatcher is set when collecting constraints during write + .dispatcher = undefined, + }; + } + + fn deepCopyContent(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, var_: types.Var, content: Content) std.mem.Allocator.Error!SnapshotContentIdx { const deep_content = switch (content) { - .flex_var => |ident| SnapshotContent{ .flex_var = ident }, - .rigid_var => |ident| SnapshotContent{ .rigid_var = ident }, - .alias => |alias| SnapshotContent{ .alias = try self.deepCopyAlias(store, alias) }, - .structure => |flat_type| SnapshotContent{ .structure = try self.deepCopyFlatType(store, flat_type) }, + .flex => |flex| SnapshotContent{ .flex = try self.deepCopyFlex(store, type_writer, var_, flex) }, + .rigid => |rigid| SnapshotContent{ .rigid = try self.deepCopyRigid(store, type_writer, rigid) }, + .alias => |alias| SnapshotContent{ .alias = try self.deepCopyAlias(store, type_writer, alias) }, + .structure => |flat_type| SnapshotContent{ .structure = try self.deepCopyFlatType(store, type_writer, flat_type) }, + .recursion_var => |rec_var| blk: { + // Snapshot the recursion var by snapshotting the structure it points to + const structure_snapshot = try self.deepCopyVarInternal(store, type_writer, rec_var.structure); + break :blk SnapshotContent{ .recursion_var = .{ .structure = structure_snapshot, .name = rec_var.name } }; + }, .err => SnapshotContent.err, }; return try self.contents.append(self.gpa, deep_content); } - fn deepCopyFlatType(self: *Self, store: *const TypesStore, flat_type: types.FlatType) std.mem.Allocator.Error!SnapshotFlatType { + fn deepCopyFlatType(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, flat_type: types.FlatType) std.mem.Allocator.Error!SnapshotFlatType { return switch (flat_type) { - .str => SnapshotFlatType.str, - .box => |box_var| { - const resolved = store.resolveVar(box_var); - const deep_content = try self.deepCopyContent(store, resolved.desc.content); - return SnapshotFlatType{ .box = deep_content }; - }, - .list => |list_var| { - const resolved = store.resolveVar(list_var); - const deep_content = try self.deepCopyContent(store, resolved.desc.content); - return SnapshotFlatType{ .list = deep_content }; - }, - .list_unbound => { - return SnapshotFlatType.list_unbound; - }, - .tuple => |tuple| SnapshotFlatType{ .tuple = try self.deepCopyTuple(store, tuple) }, - .num => |num| SnapshotFlatType{ .num = try self.deepCopyNum(store, num) }, - .nominal_type => |nominal_type| SnapshotFlatType{ .nominal_type = try self.deepCopyNominalType(store, nominal_type) }, - .fn_pure => |func| SnapshotFlatType{ .fn_pure = try self.deepCopyFunc(store, func) }, - .fn_effectful => |func| SnapshotFlatType{ .fn_effectful = try self.deepCopyFunc(store, func) }, - .fn_unbound => |func| SnapshotFlatType{ .fn_unbound = try self.deepCopyFunc(store, func) }, - .record => |record| SnapshotFlatType{ .record = try self.deepCopyRecord(store, record) }, - .record_unbound => |fields| SnapshotFlatType{ .record_unbound = try self.deepCopyRecordFields(store, fields) }, - .record_poly => |poly| SnapshotFlatType{ .record_poly = .{ - .record = try self.deepCopyRecord(store, poly.record), - .var_ = try self.deepCopyContent(store, store.resolveVar(poly.var_).desc.content), - } }, + .tuple => |tuple| SnapshotFlatType{ .tuple = try self.deepCopyTuple(store, type_writer, tuple) }, + .nominal_type => |nominal_type| SnapshotFlatType{ .nominal_type = try self.deepCopyNominalType(store, type_writer, nominal_type) }, + .fn_pure => |func| SnapshotFlatType{ .fn_pure = try self.deepCopyFunc(store, type_writer, func) }, + .fn_effectful => |func| SnapshotFlatType{ .fn_effectful = try self.deepCopyFunc(store, type_writer, func) }, + .fn_unbound => |func| SnapshotFlatType{ .fn_unbound = try self.deepCopyFunc(store, type_writer, func) }, + .record => |record| SnapshotFlatType{ .record = try self.deepCopyRecord(store, type_writer, record) }, + .record_unbound => |fields| SnapshotFlatType{ .record_unbound = try self.deepCopyRecordFields(store, type_writer, fields) }, .empty_record => SnapshotFlatType.empty_record, - .tag_union => |tag_union| SnapshotFlatType{ .tag_union = try self.deepCopyTagUnion(store, tag_union) }, + .tag_union => |tag_union| SnapshotFlatType{ .tag_union = try self.deepCopyTagUnion(store, type_writer, tag_union) }, .empty_tag_union => SnapshotFlatType.empty_tag_union, }; } - fn deepCopyAlias(self: *Self, store: *const TypesStore, alias: types.Alias) std.mem.Allocator.Error!SnapshotAlias { + fn deepCopyAlias(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, alias: types.Alias) std.mem.Allocator.Error!SnapshotAlias { + const backing_var = store.getAliasBackingVar(alias); + const deep_backing = try self.deepCopyVarInternal(store, type_writer, backing_var); + // Mark starting position in the scratch array const scratch_top = self.scratch_content.top(); - const backing_var = store.getAliasBackingVar(alias); - const backing_resolved = store.resolveVar(backing_var); - const deep_backing = try self.deepCopyContent(store, backing_resolved.desc.content); - _ = try self.scratch_content.append(self.gpa, deep_backing); - // Iterate and append to scratch array var arg_iter = store.iterAliasArgs(alias); while (arg_iter.next()) |arg_var| { - const arg_resolved = store.resolveVar(arg_var); - const deep_arg = try self.deepCopyContent(store, arg_resolved.desc.content); - _ = try self.scratch_content.append(self.gpa, deep_arg); + const deep_arg = try self.deepCopyVarInternal(store, type_writer, arg_var); + try self.scratch_content.append(deep_arg); } // Append scratch to backing array, and shrink scratch @@ -148,12 +380,12 @@ pub const Store = struct { return SnapshotAlias{ .ident = alias.ident, - .backing = try self.deepCopyContent(store, backing_resolved.desc.content), + .backing = deep_backing, .vars = args_range, }; } - fn deepCopyTuple(self: *Self, store: *const TypesStore, tuple: types.Tuple) std.mem.Allocator.Error!SnapshotTuple { + fn deepCopyTuple(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, tuple: types.Tuple) std.mem.Allocator.Error!SnapshotTuple { const elems_slice = store.sliceVars(tuple.elems); // Mark starting position in the scratch array @@ -161,9 +393,8 @@ pub const Store = struct { // Iterate and append to scratch array for (elems_slice) |elem_var| { - const elem_resolved = store.resolveVar(elem_var); - const deep_elem = try self.deepCopyContent(store, elem_resolved.desc.content); - _ = try self.scratch_content.append(self.gpa, deep_elem); + const deep_elem = try self.deepCopyVarInternal(store, type_writer, elem_var); + try self.scratch_content.append(deep_elem); } // Append scratch to backing array, and shrink scratch @@ -175,63 +406,20 @@ pub const Store = struct { }; } - fn deepCopyNum(self: *Self, store: *const TypesStore, num: types.Num) std.mem.Allocator.Error!SnapshotNum { - switch (num) { - .num_poly => |poly| { - const resolved_poly = store.resolveVar(poly.var_); - const deep_poly = try self.deepCopyContent(store, resolved_poly.desc.content); - return SnapshotNum{ .num_poly = deep_poly }; - }, - .int_poly => |poly| { - const resolved_poly = store.resolveVar(poly.var_); - const deep_poly = try self.deepCopyContent(store, resolved_poly.desc.content); - return SnapshotNum{ .int_poly = deep_poly }; - }, - .num_unbound => |requirements| { - // For unbound types, we don't have a var to resolve, just return the requirements - return SnapshotNum{ .num_unbound = requirements }; - }, - .int_unbound => |requirements| { - // For unbound types, we don't have a var to resolve, just return the requirements - return SnapshotNum{ .int_unbound = requirements }; - }, - .frac_unbound => |requirements| { - // For unbound types, we don't have a var to resolve, just return the requirements - return SnapshotNum{ .frac_unbound = requirements }; - }, - .frac_poly => |poly| { - const resolved_poly = store.resolveVar(poly.var_); - const deep_poly = try self.deepCopyContent(store, resolved_poly.desc.content); - return SnapshotNum{ .frac_poly = deep_poly }; - }, - .int_precision => |prec| { - return SnapshotNum{ .int_precision = prec }; - }, - .frac_precision => |prec| { - return SnapshotNum{ .frac_precision = prec }; - }, - .num_compact => |compact| { - return SnapshotNum{ .num_compact = compact }; - }, - } - } - - fn deepCopyNominalType(self: *Self, store: *const TypesStore, nominal_type: types.NominalType) std.mem.Allocator.Error!SnapshotNominalType { + fn deepCopyNominalType(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, nominal_type: types.NominalType) std.mem.Allocator.Error!SnapshotNominalType { // Mark starting position in the scratch array const scratch_top = self.scratch_content.top(); // Add backing var (must be first) const backing_var = store.getNominalBackingVar(nominal_type); - const backing_resolved = store.resolveVar(backing_var); - const deep_var = try self.deepCopyContent(store, backing_resolved.desc.content); - _ = try self.scratch_content.append(self.gpa, deep_var); + const deep_var = try self.deepCopyVarInternal(store, type_writer, backing_var); + try self.scratch_content.append(deep_var); // Add args after var arg_iter = store.iterNominalArgs(nominal_type); while (arg_iter.next()) |arg_var| { - const arg_resolved = store.resolveVar(arg_var); - const deep_arg = try self.deepCopyContent(store, arg_resolved.desc.content); - _ = try self.scratch_content.append(self.gpa, deep_arg); + const deep_arg = try self.deepCopyVarInternal(store, type_writer, arg_var); + try self.scratch_content.append(deep_arg); } // Append scratch to backing array, and shrink scratch @@ -245,7 +433,7 @@ pub const Store = struct { }; } - fn deepCopyFunc(self: *Self, store: *const TypesStore, func: types.Func) std.mem.Allocator.Error!SnapshotFunc { + fn deepCopyFunc(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, func: types.Func) std.mem.Allocator.Error!SnapshotFunc { const args_slice = store.sliceVars(func.args); // Mark starting position in the scratch array @@ -253,9 +441,8 @@ pub const Store = struct { // Iterate and append directly for (args_slice) |arg_var| { - const arg_resolved = store.resolveVar(arg_var); - const deep_arg = try self.deepCopyContent(store, arg_resolved.desc.content); - _ = try self.scratch_content.append(self.gpa, deep_arg); + const deep_arg = try self.deepCopyVarInternal(store, type_writer, arg_var); + try self.scratch_content.append(deep_arg); } // Append scratch to backing array, and shrink scratch @@ -263,8 +450,7 @@ pub const Store = struct { self.scratch_content.clearFrom(scratch_top); // Deep copy return type - const ret_resolved = store.resolveVar(func.ret); - const deep_ret = try self.deepCopyContent(store, ret_resolved.desc.content); + const deep_ret = try self.deepCopyVarInternal(store, type_writer, func.ret); return SnapshotFunc{ .args = args_range, @@ -273,21 +459,20 @@ pub const Store = struct { }; } - fn deepCopyRecordFields(self: *Self, store: *const TypesStore, fields: types.RecordField.SafeMultiList.Range) std.mem.Allocator.Error!SnapshotRecordFieldSafeList.Range { + fn deepCopyRecordFields(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, fields: types.RecordField.SafeMultiList.Range) std.mem.Allocator.Error!SnapshotRecordFieldSafeList.Range { // Mark starting position in the scratch array const scratch_top = self.scratch_record_fields.top(); const fields_slice = store.getRecordFieldsSlice(fields); for (fields_slice.items(.name), fields_slice.items(.var_)) |name, var_| { - const field_resolved = store.resolveVar(var_); - const deep_field_content = try self.deepCopyContent(store, field_resolved.desc.content); + const deep_field_content = try self.deepCopyVarInternal(store, type_writer, var_); const snapshot_field = SnapshotRecordField{ .name = name, .content = deep_field_content, }; - _ = try self.scratch_record_fields.append(self.gpa, snapshot_field); + try self.scratch_record_fields.append(snapshot_field); } // Append scratch to backing array, and shrink scratch @@ -297,7 +482,7 @@ pub const Store = struct { return fields_range; } - fn deepCopyRecord(self: *Self, store: *const TypesStore, record: types.Record) std.mem.Allocator.Error!SnapshotRecord { + fn deepCopyRecord(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, record: types.Record) std.mem.Allocator.Error!SnapshotRecord { // Mark starting position in the scratch array const scratch_top = self.scratch_record_fields.top(); @@ -306,15 +491,14 @@ pub const Store = struct { while (fields_iter.next()) |field_idx| { const field = store.record_fields.get(field_idx); - const field_resolved = store.resolveVar(field.var_); - const deep_field_content = try self.deepCopyContent(store, field_resolved.desc.content); + const deep_field_content = try self.deepCopyVarInternal(store, type_writer, field.var_); const snapshot_field = SnapshotRecordField{ .name = field.name, .content = deep_field_content, }; - _ = try self.scratch_record_fields.append(self.gpa, snapshot_field); + try self.scratch_record_fields.append(snapshot_field); } // Append scratch to backing array, and shrink scratch @@ -322,8 +506,7 @@ pub const Store = struct { self.scratch_record_fields.clearFrom(scratch_top); // Deep copy extension type - const ext_resolved = store.resolveVar(record.ext); - const deep_ext = try self.deepCopyContent(store, ext_resolved.desc.content); + const deep_ext = try self.deepCopyVarInternal(store, type_writer, record.ext); return SnapshotRecord{ .fields = fields_range, @@ -331,7 +514,7 @@ pub const Store = struct { }; } - fn deepCopyTagUnion(self: *Self, store: *const TypesStore, tag_union: types.TagUnion) std.mem.Allocator.Error!SnapshotTagUnion { + fn deepCopyTagUnion(self: *Self, store: *const TypesStore, type_writer: *TypeWriter, tag_union: types.TagUnion) std.mem.Allocator.Error!SnapshotTagUnion { // Mark starting position in the scratch array for tags const tags_scratch_top = self.scratch_tags.top(); @@ -347,9 +530,8 @@ pub const Store = struct { // Iterate over tag arguments and append to scratch array for (tag_args_slice) |tag_arg_var| { - const tag_arg_resolved = store.resolveVar(tag_arg_var); - const deep_tag_arg = try self.deepCopyContent(store, tag_arg_resolved.desc.content); - _ = try self.scratch_content.append(self.gpa, deep_tag_arg); + const deep_tag_arg = try self.deepCopyVarInternal(store, type_writer, tag_arg_var); + try self.scratch_content.append(deep_tag_arg); } // Append scratch to backing array, and shrink scratch @@ -362,7 +544,7 @@ pub const Store = struct { .args = tag_args_range, }; - _ = try self.scratch_tags.append(self.gpa, snapshot_tag); + try self.scratch_tags.append(snapshot_tag); } // Append scratch tags to backing array, and shrink scratch @@ -370,8 +552,7 @@ pub const Store = struct { self.scratch_tags.clearFrom(tags_scratch_top); // Deep copy extension type - const ext_resolved = store.resolveVar(tag_union.ext); - const deep_ext = try self.deepCopyContent(store, ext_resolved.desc.content); + const deep_ext = try self.deepCopyVarInternal(store, type_writer, tag_union.ext); return SnapshotTagUnion{ .tags = tags_range, @@ -379,7 +560,6 @@ pub const Store = struct { }; } - // Getter methods (similar to Store) pub fn sliceVars(self: *const Self, range: SnapshotContentIdxSafeList.Range) []const SnapshotContentIdx { return self.content_indexes.sliceRange(range); } @@ -388,740 +568,96 @@ pub const Store = struct { return self.record_fields.sliceRange(range); } + pub fn sliceStaticDispatchConstraints(self: *const Self, range: SnapshotStaticDispatchConstraintSafeList.Range) SnapshotStaticDispatchConstraintSafeList.Slice { + return self.static_dispatch_constraints.sliceRange(range); + } + + pub fn sliceTags(self: *const Self, range: SnapshotTagSafeList.Range) SnapshotTagSafeList.Slice { + return self.tags.sliceRange(range); + } + pub fn getContent(self: *const Self, idx: SnapshotContentIdx) SnapshotContent { return self.contents.get(idx).*; } + + /// Format a tag as a string, e.g. "TagName payload1 payload2" + /// Requires that all nested types have been pre-formatted via snapshotVarForError + pub fn formatTagString(self: *const Self, allocator: std.mem.Allocator, tag: SnapshotTag, idents: *const Ident.Store) ![]const u8 { + var result = std.array_list.Managed(u8).init(allocator); + errdefer result.deinit(); + + // Write tag name + const name = idents.getText(tag.name); + try result.appendSlice(name); + + // Write payload arguments using pre-stored formatted strings + const args = self.content_indexes.sliceRange(tag.args); + for (args) |arg_idx| { + try result.append(' '); + const formatted = self.getFormattedString(arg_idx) orelse ""; + try result.appendSlice(formatted); + } + + return result.toOwnedSlice(); + } }; -/// Snapshot types (no Var references!) -pub const SnapshotContent = union(enum) { - flex_var: ?Ident.Idx, - rigid_var: Ident.Idx, - alias: SnapshotAlias, - structure: SnapshotFlatType, - err, -}; +// Tests -/// TODO -pub const SnapshotAlias = struct { - ident: types.TypeIdent, - backing: SnapshotContentIdx, - vars: SnapshotContentIdxSafeList.Range, // The 1st variable is the backing var, rest are args -}; +test "formatTagString - gracefully handles missing formatted strings" { + const gpa = std.testing.allocator; -/// TODO -pub const SnapshotFlatType = union(enum) { - str, - box: SnapshotContentIdx, // Index into SnapshotStore.contents - list: SnapshotContentIdx, - list_unbound, - tuple: SnapshotTuple, - num: SnapshotNum, - nominal_type: SnapshotNominalType, - fn_pure: SnapshotFunc, - fn_effectful: SnapshotFunc, - fn_unbound: SnapshotFunc, - record: SnapshotRecord, - record_unbound: SnapshotRecordFieldSafeList.Range, - record_poly: struct { record: SnapshotRecord, var_: SnapshotContentIdx }, - empty_record, - tag_union: SnapshotTagUnion, - empty_tag_union, -}; + var store = try Store.initCapacity(gpa, 16); + defer store.deinit(); -/// TODO -pub const SnapshotTuple = struct { - elems: SnapshotContentIdxSafeList.Range, // Range into SnapshotStore.vars -}; + // Create a tag with an argument that doesn't have a formatted string + // This should use the "" fallback instead of crashing + const unknown_content_idx = try store.contents.append(gpa, .err); + const args_range = try store.content_indexes.appendSlice(gpa, &[_]SnapshotContentIdx{unknown_content_idx}); -/// TODO -pub const SnapshotNum = union(enum) { - num_poly: SnapshotContentIdx, - int_poly: SnapshotContentIdx, - frac_poly: SnapshotContentIdx, - num_unbound: types.Num.IntRequirements, - int_unbound: types.Num.IntRequirements, - frac_unbound: types.Num.FracRequirements, - int_precision: types.Num.Int.Precision, - frac_precision: types.Num.Frac.Precision, - num_compact: types.Num.Compact, -}; + // Create an ident store for the tag name + var ident_store = try Ident.Store.initCapacity(gpa, 64); + defer ident_store.deinit(gpa); + const tag_name = try ident_store.insert(gpa, Ident.for_text("MyTag")); -/// TODO -pub const SnapshotNominalType = struct { - ident: types.TypeIdent, - vars: SnapshotContentIdxSafeList.Range, // The 1st variable is the backing var, rest are args - origin_module: Ident.Idx, -}; + const tag = SnapshotTag{ + .name = tag_name, + .args = args_range, + }; -/// TODO -pub const SnapshotFunc = struct { - args: SnapshotContentIdxSafeList.Range, // Range into SnapshotStore.func_args - ret: SnapshotContentIdx, // Index into SnapshotStore.contents - needs_instantiation: bool, -}; + // Format should succeed and include the fallback placeholder + const result = try store.formatTagString(gpa, tag, &ident_store); + defer gpa.free(result); -/// TODO -pub const SnapshotRecord = struct { - fields: SnapshotRecordFieldSafeList.Range, // Range into SnapshotStore.record_fields - ext: SnapshotContentIdx, // Index into SnapshotStore.contents -}; + try std.testing.expectEqualStrings("MyTag ", result); +} -/// TODO -pub const SnapshotRecordField = struct { - name: Ident.Idx, - content: SnapshotContentIdx, // Instead of var_ -}; +test "formatTagString - uses stored formatted strings when available" { + const gpa = std.testing.allocator; -/// TODO -pub const SnapshotTagUnion = struct { - tags: SnapshotTagSafeList.Range, // Range into SnapshotStore.tags - ext: SnapshotContentIdx, // Index into SnapshotStore.contents -}; + var store = try Store.initCapacity(gpa, 16); + defer store.deinit(); -/// TODO -pub const SnapshotTag = struct { - name: Ident.Idx, - args: SnapshotContentIdxSafeList.Range, // Range into SnapshotStore.tag_args -}; + // Create a content index and store a formatted string for it + const content_idx = try store.contents.append(gpa, .err); + const formatted_str = try gpa.dupe(u8, "U64"); + try store.formatted_strings.put(gpa, content_idx, formatted_str); -const TypeContext = enum { - General, - NumContent, - ListContent, - RecordExtension, - TagUnionExtension, - RecordFieldContent, - TupleFieldContent, - FunctionArgument, - FunctionReturn, -}; + const args_range = try store.content_indexes.appendSlice(gpa, &[_]SnapshotContentIdx{content_idx}); -/// Helper that accepts a `Var` and write it as a nice string. -/// Entry point is `writeContent` -pub const SnapshotWriter = struct { - const Self = @This(); + // Create an ident store for the tag name + var ident_store = try Ident.Store.initCapacity(gpa, 64); + defer ident_store.deinit(gpa); + const tag_name = try ident_store.insert(gpa, Ident.for_text("Some")); - writer: std.ArrayList(u8).Writer, - snapshots: *const Store, - idents: *const Ident.Store, - current_module_name: ?[]const u8, - can_ir: ?*const ModuleEnv, - other_modules: ?[]const *const ModuleEnv, - next_name_index: u32, - name_counters: std.EnumMap(TypeContext, u32), + const tag = SnapshotTag{ + .name = tag_name, + .args = args_range, + }; - pub fn init(writer: std.ArrayList(u8).Writer, snapshots: *const Store, idents: *const Ident.Store) Self { - return .{ - .writer = writer, - .snapshots = snapshots, - .idents = idents, - .current_module_name = null, - .can_ir = null, - .other_modules = null, - .next_name_index = 0, - .name_counters = std.EnumMap(TypeContext, u32).init(.{}), - }; - } + // Format should use the stored formatted string + const result = try store.formatTagString(gpa, tag, &ident_store); + defer gpa.free(result); - pub fn initWithContext( - writer: std.ArrayList(u8).Writer, - snapshots: *const Store, - idents: *const Ident.Store, - current_module_name: []const u8, - can_ir: *const ModuleEnv, - other_modules: []const *const ModuleEnv, - ) Self { - return .{ - .writer = writer, - .snapshots = snapshots, - .idents = idents, - .current_module_name = current_module_name, - .can_ir = can_ir, - .other_modules = other_modules, - .next_name_index = 0, - .name_counters = std.EnumMap(TypeContext, u32).init(.{}), - }; - } - - pub fn resetContext(self: *Self) void { - self.next_name_index = 0; - self.name_counters = std.EnumMap(TypeContext, u32).init(.{}); - } - - fn generateNextName(self: *Self) !void { - // Generate name: a, b, ..., z, aa, ab, ..., az, ba, ... - // Skip any names that already exist in the identifier store - // We need at most one more name than the number of existing identifiers - const max_attempts = self.idents.interner.entry_count + 1; - var attempts: usize = 0; - while (attempts < max_attempts) : (attempts += 1) { - var n = self.next_name_index; - self.next_name_index += 1; - - var name_buf: [8]u8 = undefined; - var name_len: usize = 0; - - // Generate name in base-26: a, b, ..., z, aa, ab, ..., az, ba, ... - while (name_len < name_buf.len) { - name_buf[name_len] = @intCast('a' + (n % 26)); - name_len += 1; - n = n / 26; - if (n == 0) break; - n -= 1; - } - - // Names are generated in reverse order, so reverse the buffer - std.mem.reverse(u8, name_buf[0..name_len]); - - // Check if this name already exists in the identifier store - const candidate_name = name_buf[0..name_len]; - - // Check all identifiers in the store - const exists = self.idents.interner.contains(candidate_name); - - if (!exists) { - // This name is available, write it to the writer - for (candidate_name) |c| { - try self.writer.writeByte(c); - } - break; - } - // Name already exists, try the next one - } - - // This should never happen in practice, but let's handle it gracefully - if (attempts >= max_attempts) { - _ = try self.writer.write("var"); - try self.writer.print("{}", .{self.next_name_index}); - } - } - - fn generateContextualName(self: *Self, context: TypeContext) !void { - const base_name = switch (context) { - .NumContent => "size", - .ListContent => "elem", - .RecordExtension => "others", - .TagUnionExtension => "others", - .RecordFieldContent => "field", - .TupleFieldContent => "field", - .FunctionArgument => "arg", - .FunctionReturn => "ret", - .General => { - // Fall back to generic name generation - try self.generateNextName(); - return; - }, - }; - - // Try to generate a name with increasing counters until we find one that doesn't collide - var counter = self.name_counters.get(context) orelse 0; - var found = false; - - // We need at most as many attempts as there are existing identifiers - const max_attempts = self.idents.interner.entry_count + 1; - var attempts: usize = 0; - while (!found and attempts < max_attempts) : (attempts += 1) { - var buf: [32]u8 = undefined; - const candidate_name = if (counter == 0) - base_name - else blk: { - const name = std.fmt.bufPrint(&buf, "{s}{}", .{ base_name, counter + 1 }) catch { - // Buffer too small, fall back to generic name - try self.generateNextName(); - return; - }; - break :blk name; - }; - - // Check if this name already exists in the identifier store - const exists = self.idents.interner.contains(candidate_name); - - if (!exists) { - // This name is available, write it to the writer - for (candidate_name) |c| { - try self.writer.writeByte(c); - } - found = true; - } else { - // Try next counter - counter += 1; - } - } - - // If we couldn't find a unique contextual name, fall back to generic names - if (!found) { - try self.generateNextName(); - return; - } - - self.name_counters.put(context, counter + 1); - } - - /// Count how many times a content appears in a type - fn countOccurrences(self: *const Self, search_idx: SnapshotContentIdx, root_idx: SnapshotContentIdx) usize { - var count: usize = 0; - self.countContent(search_idx, root_idx, &count); - return count; - } - - fn countContent(self: *const Self, search_idx: SnapshotContentIdx, current_idx: SnapshotContentIdx, count: *usize) void { - if (current_idx == search_idx) { - count.* += 1; - } - - const content = self.snapshots.contents.get(current_idx); - switch (content.*) { - .flex_var, .rigid_var, .err => {}, - .alias => |alias| { - const args = self.snapshots.sliceVars(alias.vars); - for (args) |arg_idx| { - self.countContent(search_idx, arg_idx, count); - } - }, - .structure => |flat_type| { - self.countInFlatType(search_idx, flat_type, count); - }, - } - } - - fn countInFlatType(self: *const Self, search_idx: SnapshotContentIdx, flat_type: SnapshotFlatType, count: *usize) void { - switch (flat_type) { - .str, .empty_record, .empty_tag_union => {}, - .box => |sub_idx| self.countContent(search_idx, sub_idx, count), - .list => |sub_idx| self.countContent(search_idx, sub_idx, count), - .list_unbound, .num => {}, - .tuple => |tuple| { - const elems = self.snapshots.sliceVars(tuple.elems); - for (elems) |elem| { - self.countContent(search_idx, elem, count); - } - }, - .nominal_type => |nominal_type| { - const args = self.snapshots.sliceVars(nominal_type.vars); - // Skip the first var which is the nominal type's backing var - for (args[1..]) |arg_idx| { - self.countContent(search_idx, arg_idx, count); - } - }, - .fn_pure, .fn_effectful, .fn_unbound => |func| { - const args = self.snapshots.sliceVars(func.args); - for (args) |arg| { - self.countContent(search_idx, arg, count); - } - self.countContent(search_idx, func.ret, count); - }, - .record => |record| { - const fields = self.snapshots.record_fields.sliceRange(record.fields); - for (fields.items(.content)) |field_content| { - self.countContent(search_idx, field_content, count); - } - self.countContent(search_idx, record.ext, count); - }, - .record_unbound => |fields| { - const fields_slice = self.snapshots.record_fields.sliceRange(fields); - for (fields_slice.items(.content)) |field_content| { - self.countContent(search_idx, field_content, count); - } - }, - .record_poly => |poly| { - self.countInFlatType(search_idx, SnapshotFlatType{ .record = poly.record }, count); - self.countContent(search_idx, poly.var_, count); - }, - .tag_union => |tag_union| { - var iter = tag_union.tags.iterIndices(); - while (iter.next()) |tag_idx| { - const tag = self.snapshots.tags.get(tag_idx); - const args = self.snapshots.sliceVars(tag.args); - for (args) |arg_idx| { - self.countContent(search_idx, arg_idx, count); - } - } - self.countContent(search_idx, tag_union.ext, count); - }, - } - } - - /// Convert a content to a type string with context - pub fn writeWithContext(self: *Self, idx: SnapshotContentIdx, context: TypeContext, root_idx: SnapshotContentIdx) Allocator.Error!void { - const content = self.snapshots.contents.get(idx); - return self.writeContent(content.*, context, idx, root_idx); - } - - /// Convert a content to a type string - pub fn writeContent(self: *Self, content: SnapshotContent, context: TypeContext, current_idx: SnapshotContentIdx, root_idx: SnapshotContentIdx) Allocator.Error!void { - switch (content) { - .flex_var => |mb_ident_idx| { - if (mb_ident_idx) |ident_idx| { - _ = try self.writer.write(self.idents.getText(ident_idx)); - } else { - // Check if this variable appears multiple times - const occurrences = self.countOccurrences(current_idx, root_idx); - if (occurrences == 1) { - _ = try self.writer.write("_"); - } - try self.generateContextualName(context); - } - }, - .rigid_var => |ident_idx| { - _ = try self.writer.write(self.idents.getText(ident_idx)); - }, - .alias => |alias| { - try self.writeAlias(alias, root_idx); - }, - .structure => |flat_type| { - try self.writeFlatType(flat_type, root_idx); - }, - .err => { - _ = try self.writer.write("Error"); - }, - } - } - - /// Write an alias type - pub fn writeAlias(self: *Self, alias: SnapshotAlias, root_idx: SnapshotContentIdx) Allocator.Error!void { - _ = try self.writer.write(self.idents.getText(alias.ident.ident_idx)); - - // The 1st var is the alias type's backing var, so we skip it - var vars = self.snapshots.sliceVars(alias.vars); - std.debug.assert(vars.len > 0); - vars = vars[1..]; - - if (vars.len > 0) { - _ = try self.writer.write("("); - for (vars, 0..) |arg, i| { - if (i > 0) _ = try self.writer.write(", "); - try self.writeWithContext(arg, .General, root_idx); - } - _ = try self.writer.write(")"); - } - } - - /// Convert a flat type to a type string - pub fn writeFlatType(self: *Self, flat_type: SnapshotFlatType, root_idx: SnapshotContentIdx) Allocator.Error!void { - switch (flat_type) { - .str => { - _ = try self.writer.write("Str"); - }, - .box => |sub_var| { - _ = try self.writer.write("Box("); - try self.writeWithContext(sub_var, .General, root_idx); - _ = try self.writer.write(")"); - }, - .list => |sub_var| { - _ = try self.writer.write("List("); - try self.writeWithContext(sub_var, .ListContent, root_idx); - _ = try self.writer.write(")"); - }, - .list_unbound => { - _ = try self.writer.write("List(_"); - try self.generateContextualName(.ListContent); - _ = try self.writer.write(")"); - }, - .tuple => |tuple| { - try self.writeTuple(tuple, root_idx); - }, - .num => |num| { - try self.writeNum(num, root_idx); - }, - .nominal_type => |nominal_type| { - try self.writeNominalType(nominal_type, root_idx); - }, - .fn_pure => |func| { - try self.writeFuncWithArrow(func, " -> ", root_idx); - }, - .fn_effectful => |func| { - try self.writeFuncWithArrow(func, " => ", root_idx); - }, - .fn_unbound => |func| { - try self.writeFuncWithArrow(func, " -> ", root_idx); - }, - .record => |record| { - try self.writeRecord(record, root_idx); - }, - .record_unbound => |fields| { - try self.writeRecordFields(fields, root_idx); - }, - .record_poly => |poly| { - try self.writeRecord(poly.record, root_idx); - try self.writeWithContext(poly.var_, .General, root_idx); - }, - .empty_record => { - _ = try self.writer.write("{}"); - }, - .tag_union => |tag_union| { - try self.writeTagUnion(tag_union, root_idx); - }, - .empty_tag_union => { - _ = try self.writer.write("[]"); - }, - } - } - - /// Write a tuple type - pub fn writeTuple(self: *Self, tuple: SnapshotTuple, root_idx: SnapshotContentIdx) Allocator.Error!void { - const elems = self.snapshots.sliceVars(tuple.elems); - _ = try self.writer.write("("); - for (elems, 0..) |elem, i| { - if (i > 0) _ = try self.writer.write(", "); - try self.writeWithContext(elem, .TupleFieldContent, root_idx); - } - _ = try self.writer.write(")"); - } - - /// Write a nominal type - pub fn writeNominalType(self: *Self, nominal_type: SnapshotNominalType, root_idx: SnapshotContentIdx) Allocator.Error!void { - _ = try self.writer.write(self.idents.getText(nominal_type.ident.ident_idx)); - - // The 1st var is the nominal type's backing var, so we skip it - var vars = self.snapshots.sliceVars(nominal_type.vars); - std.debug.assert(vars.len > 0); - vars = vars[1..]; - - if (vars.len > 0) { - _ = try self.writer.write("("); - for (vars, 0..) |arg, i| { - if (i > 0) _ = try self.writer.write(", "); - try self.writeWithContext(arg, .General, root_idx); - } - _ = try self.writer.write(")"); - } - - // Add origin information if it's from a different module - if (self.current_module_name) |current_module| { - const origin_module_name = self.idents.getText(nominal_type.origin_module); - - // Only show origin if it's different from the current module - if (!std.mem.eql(u8, origin_module_name, current_module)) { - _ = try self.writer.write(" (from "); - _ = try self.writer.write(origin_module_name); - _ = try self.writer.write(")"); - } - } - } - - /// Convert a content to a type string - pub fn write(self: *Self, idx: SnapshotContentIdx) Allocator.Error!void { - try self.writeWithContext(idx, .General, idx); - } - - /// Write a function type with a specific arrow - pub fn writeFuncWithArrow(self: *Self, func: SnapshotFunc, arrow: []const u8, root_idx: SnapshotContentIdx) Allocator.Error!void { - const args = self.snapshots.sliceVars(func.args); - - // Write arguments - if (args.len == 0) { - _ = try self.writer.write("({})"); - } else if (args.len == 1) { - try self.writeWithContext(args[0], .FunctionArgument, root_idx); - } else { - for (args, 0..) |arg, i| { - if (i > 0) _ = try self.writer.write(", "); - try self.writeWithContext(arg, .FunctionArgument, root_idx); - } - } - - _ = try self.writer.write(arrow); - - try self.writeWithContext(func.ret, .FunctionReturn, root_idx); - } - - /// Write a record type - pub fn writeRecord(self: *Self, record: SnapshotRecord, root_idx: SnapshotContentIdx) Allocator.Error!void { - _ = try self.writer.write("{ "); - - const fields_slice = self.snapshots.record_fields.sliceRange(record.fields); - - if (fields_slice.len > 0) { - // Write first field - _ = try self.writer.write(self.idents.getText(fields_slice.items(.name)[0])); - _ = try self.writer.write(": "); - try self.writeWithContext(fields_slice.items(.content)[0], .RecordFieldContent, root_idx); - - // Write remaining fields - for (fields_slice.items(.name)[1..], fields_slice.items(.content)[1..]) |name, content| { - _ = try self.writer.write(", "); - _ = try self.writer.write(self.idents.getText(name)); - _ = try self.writer.write(": "); - try self.writeWithContext(content, .RecordFieldContent, root_idx); - } - } - - // Show extension variable if it's not empty - switch (self.snapshots.contents.get(record.ext).*) { - .structure => |flat_type| switch (flat_type) { - .empty_record => {}, // Don't show empty extension - else => { - if (fields_slice.len > 0) _ = try self.writer.write(", "); - try self.writeWithContext(record.ext, .RecordExtension, root_idx); - }, - }, - else => { - if (fields_slice.len > 0) _ = try self.writer.write(", "); - try self.writeWithContext(record.ext, .RecordExtension, root_idx); - }, - } - - _ = try self.writer.write(" }"); - } - - /// Write record fields without extension - pub fn writeRecordFields(self: *Self, fields: SnapshotRecordFieldSafeList.Range, root_idx: SnapshotContentIdx) Allocator.Error!void { - if (fields.isEmpty()) { - _ = try self.writer.write("{}"); - return; - } - - const fields_slice = self.snapshots.record_fields.sliceRange(fields); - - _ = try self.writer.write("{ "); - - // Write first field - we already verified that there is at least one field. - _ = try self.writer.write(self.idents.getText(fields_slice.items(.name)[0])); - _ = try self.writer.write(": "); - try self.writeWithContext(fields_slice.items(.content)[0], .RecordFieldContent, root_idx); - - // Write remaining fields - for (fields_slice.items(.name)[1..], fields_slice.items(.content)[1..]) |name, content| { - _ = try self.writer.write(", "); - _ = try self.writer.write(self.idents.getText(name)); - _ = try self.writer.write(": "); - try self.writeWithContext(content, .RecordFieldContent, root_idx); - } - - _ = try self.writer.write(" }"); - } - - /// Write a tag union - pub fn writeTagUnion(self: *Self, tag_union: SnapshotTagUnion, root_idx: SnapshotContentIdx) Allocator.Error!void { - _ = try self.writer.write("["); - var iter = tag_union.tags.iterIndices(); - while (iter.next()) |tag_idx| { - if (@intFromEnum(tag_idx) > @intFromEnum(tag_union.tags.start)) { - _ = try self.writer.write(", "); - } - - const tag = self.snapshots.tags.get(tag_idx); - try self.writeTag(tag, root_idx); - } - - _ = try self.writer.write("]"); - - // Show extension variable if it's not empty - switch (self.snapshots.contents.get(tag_union.ext).*) { - .flex_var => |mb_ident| { - if (mb_ident) |ident_idx| { - _ = try self.writer.write(self.idents.getText(ident_idx)); - } else { - // Check if this variable appears multiple times - const occurrences = self.countOccurrences(tag_union.ext, root_idx); - if (occurrences == 1) { - _ = try self.writer.write("_"); - } - try self.generateContextualName(.TagUnionExtension); - } - }, - .structure => |flat_type| switch (flat_type) { - .empty_tag_union => {}, // Don't show empty extension - else => { - try self.writeWithContext(tag_union.ext, .TagUnionExtension, root_idx); - }, - }, - .rigid_var => |ident_idx| { - _ = try self.writer.write(self.idents.getText(ident_idx)); - }, - else => { - try self.writeWithContext(tag_union.ext, .TagUnionExtension, root_idx); - }, - } - } - - /// Write a single tag - pub fn writeTag(self: *Self, tag: SnapshotTag, root_idx: SnapshotContentIdx) Allocator.Error!void { - _ = try self.writer.write(self.idents.getText(tag.name)); - const args = self.snapshots.sliceVars(tag.args); - if (args.len > 0) { - _ = try self.writer.write("("); - for (args, 0..) |arg, i| { - if (i > 0) _ = try self.writer.write(", "); - try self.writeWithContext(arg, .General, root_idx); - } - _ = try self.writer.write(")"); - } - } - - /// Convert a num type to a type string - pub fn writeNum(self: *Self, num: SnapshotNum, root_idx: SnapshotContentIdx) Allocator.Error!void { - switch (num) { - .num_poly => |sub_var| { - _ = try self.writer.write("Num("); - try self.writeWithContext(sub_var, .NumContent, root_idx); - _ = try self.writer.write(")"); - }, - .int_poly => |sub_var| { - _ = try self.writer.write("Int("); - try self.writeWithContext(sub_var, .NumContent, root_idx); - _ = try self.writer.write(")"); - }, - .frac_poly => |sub_var| { - _ = try self.writer.write("Frac("); - try self.writeWithContext(sub_var, .NumContent, root_idx); - _ = try self.writer.write(")"); - }, - .num_unbound => |_| { - _ = try self.writer.write("Num(_"); - try self.generateContextualName(.NumContent); - _ = try self.writer.write(")"); - }, - .int_unbound => |_| { - _ = try self.writer.write("Int(_"); - try self.generateContextualName(.NumContent); - _ = try self.writer.write(")"); - }, - .frac_unbound => |_| { - _ = try self.writer.write("Frac(_"); - try self.generateContextualName(.NumContent); - _ = try self.writer.write(")"); - }, - .int_precision => |prec| { - try self.writeIntType(prec); - }, - .frac_precision => |prec| { - try self.writeFracType(prec); - }, - .num_compact => |compact| { - switch (compact) { - .int => |prec| { - try self.writeIntType(prec); - }, - .frac => |prec| { - try self.writeFracType(prec); - }, - } - }, - } - } - - pub fn writeIntType(self: *Self, prec: types.Num.Int.Precision) Allocator.Error!void { - _ = switch (prec) { - .u8 => try self.writer.write("U8"), - .i8 => try self.writer.write("I8"), - .u16 => try self.writer.write("U16"), - .i16 => try self.writer.write("I16"), - .u32 => try self.writer.write("U32"), - .i32 => try self.writer.write("I32"), - .u64 => try self.writer.write("U64"), - .i64 => try self.writer.write("I64"), - .u128 => try self.writer.write("U128"), - .i128 => try self.writer.write("I128"), - }; - } - - pub fn writeFracType(self: *Self, prec: types.Num.Frac.Precision) Allocator.Error!void { - _ = switch (prec) { - .f32 => try self.writer.write("F32"), - .f64 => try self.writer.write("F64"), - .dec => try self.writer.write("Dec"), - }; - } -}; + try std.testing.expectEqualStrings("Some U64", result); +} diff --git a/src/check/test/TestEnv.zig b/src/check/test/TestEnv.zig new file mode 100644 index 0000000000..0cc1279bf1 --- /dev/null +++ b/src/check/test/TestEnv.zig @@ -0,0 +1,652 @@ +//! Test environment for canonicalization testing, providing utilities to parse, canonicalize, and inspect Roc expressions. + +const std = @import("std"); +const base = @import("base"); +const types = @import("types"); +const parse = @import("parse"); +const CIR = @import("can").CIR; +const Can = @import("can").Can; +const ModuleEnv = @import("can").ModuleEnv; +const collections = @import("collections"); + +const Check = @import("../Check.zig"); +const problem_mod = @import("../problem.zig"); + +const CommonEnv = base.CommonEnv; +const testing = std.testing; + +const compiled_builtins = @import("compiled_builtins"); + +/// Wrapper for a loaded compiled module that tracks the buffer +const LoadedModule = struct { + env: *ModuleEnv, + buffer: []align(collections.CompactWriter.SERIALIZATION_ALIGNMENT.toByteUnits()) u8, + gpa: std.mem.Allocator, + + fn deinit(self: *LoadedModule) void { + // Only free the hashmap that was allocated during deserialization + // Most other data (like the SafeList contents) points into the buffer + self.env.imports.map.deinit(self.gpa); + + // Free the buffer (the env points into this buffer for most data) + self.gpa.free(self.buffer); + // Free the env struct itself + self.gpa.destroy(self.env); + } +}; + +/// Deserialize BuiltinIndices from the binary data generated at build time +fn deserializeBuiltinIndices(gpa: std.mem.Allocator, bin_data: []const u8) !CIR.BuiltinIndices { + // Copy to properly aligned memory + const aligned_buffer = try gpa.alignedAlloc(u8, @enumFromInt(@alignOf(CIR.BuiltinIndices)), bin_data.len); + defer gpa.free(aligned_buffer); + @memcpy(aligned_buffer, bin_data); + + const indices_ptr = @as(*const CIR.BuiltinIndices, @ptrCast(aligned_buffer.ptr)); + return indices_ptr.*; +} + +/// Load a compiled ModuleEnv from embedded binary data +fn loadCompiledModule(gpa: std.mem.Allocator, bin_data: []const u8, module_name: []const u8, source: []const u8) !LoadedModule { + // Copy the embedded data to properly aligned memory + // CompactWriter requires specific alignment for serialization + const CompactWriter = collections.CompactWriter; + const buffer = try gpa.alignedAlloc(u8, CompactWriter.SERIALIZATION_ALIGNMENT, bin_data.len); + @memcpy(buffer, bin_data); + + // Cast to the serialized structure + const serialized_ptr = @as( + *ModuleEnv.Serialized, + @ptrCast(@alignCast(buffer.ptr)), + ); + + const env = try gpa.create(ModuleEnv); + errdefer gpa.destroy(env); + + // Deserialize + const base_ptr = @intFromPtr(buffer.ptr); + + // Deserialize common env first so we can look up identifiers + const common = serialized_ptr.common.deserialize(@as(i64, @intCast(base_ptr)), source).*; + + env.* = ModuleEnv{ + .gpa = gpa, + .common = common, + .types = serialized_ptr.types.deserialize(@as(i64, @intCast(base_ptr)), gpa).*, // Pass gpa to types deserialize + .module_kind = serialized_ptr.module_kind.decode(), + .all_defs = serialized_ptr.all_defs, + .all_statements = serialized_ptr.all_statements, + .exports = serialized_ptr.exports, + .requires_types = serialized_ptr.requires_types.deserialize(@as(i64, @intCast(base_ptr))).*, + .for_clause_aliases = serialized_ptr.for_clause_aliases.deserialize(@as(i64, @intCast(base_ptr))).*, + .builtin_statements = serialized_ptr.builtin_statements, + .external_decls = serialized_ptr.external_decls.deserialize(@as(i64, @intCast(base_ptr))).*, + .imports = (try serialized_ptr.imports.deserialize(@as(i64, @intCast(base_ptr)), gpa)).*, + .module_name = module_name, + .module_name_idx = undefined, // Not used for deserialized modules (only needed during fresh canonicalization) + .diagnostics = serialized_ptr.diagnostics, + .store = serialized_ptr.store.deserialize(@as(i64, @intCast(base_ptr)), gpa).*, + .evaluation_order = null, + .idents = ModuleEnv.CommonIdents.find(&common), + .deferred_numeric_literals = try ModuleEnv.DeferredNumericLiteral.SafeList.initCapacity(gpa, 0), + .import_mapping = types.import_mapping.ImportMapping.init(gpa), + .method_idents = serialized_ptr.method_idents.deserialize(@as(i64, @intCast(base_ptr))).*, + .rigid_vars = std.AutoHashMapUnmanaged(base.Ident.Idx, types.Var){}, + }; + + return LoadedModule{ + .env = env, + .buffer = buffer, + .gpa = gpa, + }; +} + +gpa: std.mem.Allocator, +module_env: *ModuleEnv, +parse_ast: *parse.AST, +can: *Can, +checker: Check, +type_writer: types.TypeWriter, + +module_envs: std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType), + +// Loaded Builtin module (loaded per test, cleaned up in deinit) +builtin_module: LoadedModule, +// Whether this TestEnv owns the builtin_module and should deinit it +owns_builtin_module: bool, +/// Heap-allocated source buffer owned by this TestEnv (if any) +owned_source: ?[]u8 = null, + +/// Test environment for canonicalization testing, providing a convenient wrapper around ModuleEnv, AST, and Can. +const TestEnv = @This(); + +/// Initialize where the provided source is an entire file +/// +/// Accepts another module that should already be can'd and type checked, and will +/// add that module as an import to this module. +/// IMPORTANT: This reuses the Builtin module from the imported module to ensure +/// type variables from auto-imported types (Bool, Try, Str) are shared across modules. +pub fn initWithImport(module_name: []const u8, source: []const u8, other_module_name: []const u8, other_test_env: *const TestEnv) !TestEnv { + const gpa = std.testing.allocator; + + // Allocate our ModuleEnv, AST, and Can on the heap + // so we can keep them around for testing purposes... + // this is an unusual setup, but helps us with testing + const module_env: *ModuleEnv = try gpa.create(ModuleEnv); + errdefer gpa.destroy(module_env); + + const parse_ast = try gpa.create(parse.AST); + errdefer gpa.destroy(parse_ast); + + const can = try gpa.create(Can); + errdefer gpa.destroy(can); + + var module_envs = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(gpa); + + std.debug.assert(!std.mem.eql(u8, module_name, other_module_name)); + + // Reuse the Builtin module from the imported module + // This ensures type variables for auto-imported types (Bool, Try, Str) are shared + const builtin_indices = try deserializeBuiltinIndices(gpa, compiled_builtins.builtin_indices_bin); + const builtin_env = other_test_env.builtin_module.env; + + // Initialize the module_env so we can use its ident store + module_env.* = try ModuleEnv.init(gpa, source); + errdefer module_env.deinit(); + + module_env.common.source = source; + module_env.module_name = module_name; + module_env.module_name_idx = try module_env.insertIdent(base.Ident.for_text(module_name)); + try module_env.common.calcLineStarts(gpa); + + // Put the other module in the env map using module_env's ident store + const other_module_ident = try module_env.insertIdent(base.Ident.for_text(other_module_name)); + + // For type modules, look up the exposed type statement index + // The type name matches the module name for type modules + const statement_idx = blk: { + if (other_test_env.module_env.module_kind == .type_module) { + // Type modules expose their main type under the module name + const type_ident = other_test_env.module_env.common.findIdent(other_module_name); + if (type_ident) |ident| { + if (other_test_env.module_env.getExposedNodeIndexById(ident)) |node_idx| { + // The node index IS the statement index for type declarations + break :blk @as(CIR.Statement.Idx, @enumFromInt(node_idx)); + } + } + } + break :blk null; + }; + + // For user modules, the qualified name is just the module name itself + // Note: Insert into module_env (calling module), not other_test_env.module_env (target module) + // since Ident.Idx values are not transferable between stores. + const other_qualified_ident = try module_env.insertIdent(base.Ident.for_text(other_module_name)); + try module_envs.put(other_module_ident, .{ + .env = other_test_env.module_env, + .statement_idx = statement_idx, + .qualified_type_ident = other_qualified_ident, + }); + + // Populate module_envs with Bool, Try, Dict, Set using shared function + // This ensures production and tests use identical logic + try Can.populateModuleEnvs( + &module_envs, + module_env, + builtin_env, + builtin_indices, + ); + + // Parse the AST + parse_ast.* = try parse.parse(&module_env.common, gpa); + errdefer parse_ast.deinit(gpa); + parse_ast.store.emptyScratch(); + + // Canonicalize + try module_env.initCIRFields(module_name); + + can.* = try Can.init(module_env, parse_ast, &module_envs); + errdefer can.deinit(); + + try can.canonicalizeFile(); + try can.validateForChecking(); + + // Get Bool, Try, and Str statement indices from the IMPORTED modules (not copied!) + const bool_stmt_in_bool_module = builtin_indices.bool_type; + const try_stmt_in_result_module = builtin_indices.try_type; + const str_stmt_in_builtin_module = builtin_indices.str_type; + + const module_builtin_ctx: Check.BuiltinContext = .{ + .module_name = try module_env.insertIdent(base.Ident.for_text(module_name)), + .bool_stmt = bool_stmt_in_bool_module, + .try_stmt = try_stmt_in_result_module, + .str_stmt = str_stmt_in_builtin_module, + .builtin_module = other_test_env.builtin_module.env, + .builtin_indices = builtin_indices, + }; + + // Build imported_envs array + // Always include the builtin module for auto-imported types (Bool, Str, etc.) + var imported_envs = try std.ArrayList(*const ModuleEnv).initCapacity(gpa, 2); + defer imported_envs.deinit(gpa); + + // Add builtin module unconditionally (needed for auto-imported types) + try imported_envs.append(gpa, other_test_env.builtin_module.env); + + // Process explicit imports + const import_count = module_env.imports.imports.items.items.len; + for (module_env.imports.imports.items.items[0..import_count]) |str_idx| { + const import_name = module_env.getString(str_idx); + if (std.mem.eql(u8, import_name, other_module_name)) { + // Cross-module import - append the other test module's env + try imported_envs.append(gpa, other_test_env.module_env); + } + } + + // Resolve imports - map each import to its index in imported_envs + module_env.imports.resolveImports(module_env, imported_envs.items); + + // Type Check - Pass all imported modules + var checker = try Check.init( + gpa, + &module_env.types, + module_env, + imported_envs.items, + &module_envs, + &module_env.store.regions, + module_builtin_ctx, + ); + errdefer checker.deinit(); + + try checker.checkFile(); + + var type_writer = try module_env.initTypeWriter(); + errdefer type_writer.deinit(); + + return TestEnv{ + .gpa = gpa, + .module_env = module_env, + .parse_ast = parse_ast, + .can = can, + .checker = checker, + .type_writer = type_writer, + .module_envs = module_envs, + .builtin_module = other_test_env.builtin_module, + .owns_builtin_module = false, // Borrowed from other_test_env + }; +} + +/// Initialize where the provided source is an entire file +pub fn init(module_name: []const u8, source: []const u8) !TestEnv { + const gpa = std.testing.allocator; + + // Allocate our ModuleEnv, AST, and Can on the heap + // so we can keep them around for testing purposes... + // this is an unusual setup, but helps us with testing + const module_env: *ModuleEnv = try gpa.create(ModuleEnv); + errdefer gpa.destroy(module_env); + + const parse_ast = try gpa.create(parse.AST); + errdefer gpa.destroy(parse_ast); + + const can = try gpa.create(Can); + errdefer gpa.destroy(can); + + var module_envs = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(gpa); + + // Load Builtin module once - Bool, Try, and Str are all types within this module + const builtin_indices = try deserializeBuiltinIndices(gpa, compiled_builtins.builtin_indices_bin); + var builtin_module = try loadCompiledModule(gpa, compiled_builtins.builtin_bin, "Builtin", compiled_builtins.builtin_source); + errdefer builtin_module.deinit(); + + // Initialize the ModuleEnv with the CommonEnv + module_env.* = try ModuleEnv.init(gpa, source); + errdefer module_env.deinit(); + + module_env.common.source = source; + module_env.module_name = module_name; + module_env.module_name_idx = try module_env.insertIdent(base.Ident.for_text(module_name)); + try module_env.common.calcLineStarts(gpa); + + // Populate module_envs with Bool, Try, Dict, Set using shared function + // This ensures production and tests use identical logic + try Can.populateModuleEnvs( + &module_envs, + module_env, + builtin_module.env, + builtin_indices, + ); + + // Parse the AST + parse_ast.* = try parse.parse(&module_env.common, gpa); + errdefer parse_ast.deinit(gpa); + parse_ast.store.emptyScratch(); + + // Canonicalize + try module_env.initCIRFields(module_name); + + can.* = try Can.init(module_env, parse_ast, &module_envs); + errdefer can.deinit(); + + try can.canonicalizeFile(); + try can.validateForChecking(); + + // Get Bool, Try, and Str statement indices from the IMPORTED modules (not copied!) + const bool_stmt_in_bool_module = builtin_indices.bool_type; + const try_stmt_in_result_module = builtin_indices.try_type; + const str_stmt_in_builtin_module = builtin_indices.str_type; + + const module_builtin_ctx: Check.BuiltinContext = .{ + .module_name = try module_env.insertIdent(base.Ident.for_text(module_name)), + .bool_stmt = bool_stmt_in_bool_module, + .try_stmt = try_stmt_in_result_module, + .str_stmt = str_stmt_in_builtin_module, + .builtin_module = builtin_module.env, + .builtin_indices = builtin_indices, + }; + + // Build imported_envs array + // Always include the builtin module for auto-imported types (Bool, Str, etc.) + var imported_envs = try std.ArrayList(*const ModuleEnv).initCapacity(gpa, 2); + defer imported_envs.deinit(gpa); + + // Add builtin module unconditionally (needed for auto-imported types) + try imported_envs.append(gpa, builtin_module.env); + + // Resolve imports - map each import to its index in imported_envs + module_env.imports.resolveImports(module_env, imported_envs.items); + + // Type Check - Pass the imported modules in other_modules parameter + var checker = try Check.init( + gpa, + &module_env.types, + module_env, + imported_envs.items, + &module_envs, + &module_env.store.regions, + module_builtin_ctx, + ); + errdefer checker.deinit(); + + try checker.checkFile(); + + var type_writer = try module_env.initTypeWriter(); + errdefer type_writer.deinit(); + + return TestEnv{ + .gpa = gpa, + .module_env = module_env, + .parse_ast = parse_ast, + .can = can, + .checker = checker, + .type_writer = type_writer, + .module_envs = module_envs, + .builtin_module = builtin_module, + .owns_builtin_module = true, // We own this module + }; +} + +/// Initialize where the provided source a single expression +pub fn initExpr(module_name: []const u8, comptime source_expr: []const u8) !TestEnv { + const gpa = std.testing.allocator; + + const source_wrapper = + \\main = + ; + + const total_len = source_wrapper.len + 1 + source_expr.len; + var source = try gpa.alloc(u8, total_len); + errdefer gpa.free(source); + + std.mem.copyForwards(u8, source[0..source_wrapper.len], source_wrapper); + source[source_wrapper.len] = ' '; + std.mem.copyForwards(u8, source[source_wrapper.len + 1 ..], source_expr); + + var test_env = try TestEnv.init(module_name, source); + test_env.owned_source = source; + return test_env; +} + +pub fn deinit(self: *TestEnv) void { + self.can.deinit(); + self.gpa.destroy(self.can); + self.parse_ast.deinit(self.gpa); + self.gpa.destroy(self.parse_ast); + + self.checker.deinit(); + self.type_writer.deinit(); + + // ModuleEnv.deinit calls self.common.deinit() to clean up CommonEnv's internals + // Since common is now a value field, we don't need to free it separately + self.module_env.deinit(); + self.gpa.destroy(self.module_env); + + if (self.owned_source) |buffer| { + self.gpa.free(buffer); + } + + self.module_envs.deinit(); + + // Clean up loaded Builtin module (only if we own it) + if (self.owns_builtin_module) { + self.builtin_module.deinit(); + } +} + +/// Get the inferred type of the last declaration and compare it to the provided +/// expected type string. +/// +/// Also assert that there were no problems processing the source code. +pub fn assertDefType(self: *TestEnv, target_def_name: []const u8, expected: []const u8) !void { + try self.assertNoErrors(); + + try testing.expect(self.module_env.all_defs.span.len > 0); + + const idents = self.module_env.getIdentStoreConst(); + const defs_slice = self.module_env.store.sliceDefs(self.module_env.all_defs); + for (defs_slice) |def_idx| { + const def = self.module_env.store.getDef(def_idx); + const ptrn = self.module_env.store.getPattern(def.pattern); + + switch (ptrn) { + .assign => |assign| { + const def_name = idents.getText(assign.ident); + if (std.mem.eql(u8, target_def_name, def_name)) { + try self.type_writer.write(ModuleEnv.varFrom(def_idx), .wrap); + try testing.expectEqualStrings(expected, self.type_writer.get()); + return; + } + }, + else => { + return error.TestUnexpectedResult; + }, + } + } + return error.TestUnexpectedResult; +} + +/// Get the inferred type of the last declaration and compare it to the provided +/// expected type string. +/// +/// Also assert that there were no problems processing the source code. +pub fn assertLastDefType(self: *TestEnv, expected: []const u8) !void { + try self.assertNoErrors(); + + try testing.expect(self.module_env.all_defs.span.len > 0); + const defs_slice = self.module_env.store.sliceDefs(self.module_env.all_defs); + const last_def_idx = defs_slice[defs_slice.len - 1]; + const last_def_var = ModuleEnv.varFrom(last_def_idx); + + try self.type_writer.write(last_def_var, .wrap); + try testing.expectEqualStrings(expected, self.type_writer.get()); +} + +/// Assert that the last definition's type contains the given substring +pub fn assertLastDefTypeContains(self: *TestEnv, expected_substring: []const u8) !void { + try self.assertNoErrors(); + + try testing.expect(self.module_env.all_defs.span.len > 0); + const defs_slice = self.module_env.store.sliceDefs(self.module_env.all_defs); + const last_def_idx = defs_slice[defs_slice.len - 1]; + const last_def_var = ModuleEnv.varFrom(last_def_idx); + + try self.type_writer.write(last_def_var, .wrap); + const type_str = self.type_writer.get(); + if (std.mem.indexOf(u8, type_str, expected_substring) == null) { + std.debug.print("Expected type to contain '{s}', but got: {s}\n", .{ expected_substring, type_str }); + return error.TestExpectedEqual; + } +} + +/// Get the inferred type descriptor of the last declaration +/// +/// Also assert that there were no problems processing the source code. +pub fn getLastExprType(self: *TestEnv) !types.Descriptor { + try self.assertNoParseProblems(); + // try self.assertNoCanProblems(); + try self.assertNoTypeProblems(); + + try testing.expect(self.module_env.all_defs.span.len > 0); + const defs_slice = self.module_env.store.sliceDefs(self.module_env.all_defs); + const last_def_idx = defs_slice[defs_slice.len - 1]; + + return self.module_env.types.resolveVar(ModuleEnv.varFrom(last_def_idx)).desc; +} + +/// Assert that there were no parse, canonicalization, or type checking errors. +pub fn assertNoErrors(self: *TestEnv) !void { + try self.assertNoParseProblems(); + try self.assertNoCanProblems(); + try self.assertNoTypeProblems(); +} + +/// Assert that there was a single type error when checking the input. Assert +/// that the title of the type error matches the expected title. +pub fn assertOneTypeError(self: *TestEnv, expected: []const u8) !void { + try self.assertNoParseProblems(); + // try self.assertNoCanProblems(); + + // Assert 1 problem + try testing.expectEqual(1, self.checker.problems.problems.items.len); + const problem = self.checker.problems.problems.items[0]; + + // Assert the rendered problem matches the expected problem + var report_builder = problem_mod.ReportBuilder.init( + self.gpa, + self.module_env, + self.module_env, + &self.checker.snapshots, + "test", + &.{}, + &self.checker.import_mapping, + ); + defer report_builder.deinit(); + + var report = try report_builder.build(problem); + defer report.deinit(); + + try testing.expectEqualStrings(expected, report.title); +} + +/// Assert that the first type error matches the expected title (allows multiple errors). +pub fn assertFirstTypeError(self: *TestEnv, expected: []const u8) !void { + try self.assertNoParseProblems(); + + // Assert at least 1 problem + try testing.expect(self.checker.problems.problems.items.len >= 1); + const problem = self.checker.problems.problems.items[0]; + + // Assert the rendered problem matches the expected problem + var report_builder = problem_mod.ReportBuilder.init( + self.gpa, + self.module_env, + self.module_env, + &self.checker.snapshots, + "test", + &.{}, + &self.checker.import_mapping, + ); + defer report_builder.deinit(); + + var report = try report_builder.build(problem); + defer report.deinit(); + + try testing.expectEqualStrings(expected, report.title); +} + +fn renderReportToMarkdownBuffer(buf: *std.array_list.Managed(u8), report: anytype) !void { + buf.clearRetainingCapacity(); + var unmanaged = buf.moveToUnmanaged(); + defer buf.* = unmanaged.toManaged(buf.allocator); + + var writer_alloc = std.Io.Writer.Allocating.fromArrayList(buf.allocator, &unmanaged); + defer unmanaged = writer_alloc.toArrayList(); + + report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) { + error.WriteFailed => return error.OutOfMemory, + else => return err, + }; +} + +fn assertNoParseProblems(self: *TestEnv) !void { + if (self.parse_ast.hasErrors()) { + var report_buf = try std.array_list.Managed(u8).initCapacity(self.gpa, 256); + defer report_buf.deinit(); + + for (self.parse_ast.tokenize_diagnostics.items) |tok_diag| { + var report = try self.parse_ast.tokenizeDiagnosticToReport(tok_diag, self.gpa, null); + defer report.deinit(); + + try renderReportToMarkdownBuffer(&report_buf, &report); + try testing.expectEqualStrings("EXPECTED NO ERROR", report_buf.items); + } + + for (self.parse_ast.parse_diagnostics.items) |diag| { + var report = try self.parse_ast.parseDiagnosticToReport(&self.module_env.common, diag, self.gpa, self.module_env.module_name); + defer report.deinit(); + + try renderReportToMarkdownBuffer(&report_buf, &report); + try testing.expectEqualStrings("EXPECTED NO ERROR", report_buf.items); + } + } +} + +fn assertNoCanProblems(self: *TestEnv) !void { + var report_buf = try std.array_list.Managed(u8).initCapacity(self.gpa, 256); + defer report_buf.deinit(); + + const diagnostics = try self.module_env.getDiagnostics(); + defer self.gpa.free(diagnostics); + + for (diagnostics) |d| { + var report = try self.module_env.diagnosticToReport(d, self.gpa, self.module_env.module_name); + defer report.deinit(); + + try renderReportToMarkdownBuffer(&report_buf, &report); + + // Ignore "MISSING MAIN! FUNCTION" error - it's expected in test modules + if (std.mem.indexOf(u8, report_buf.items, "MISSING MAIN! FUNCTION") != null) { + continue; + } + + try testing.expectEqualStrings("EXPECTED NO ERROR", report_buf.items); + } +} + +fn assertNoTypeProblems(self: *TestEnv) !void { + var report_builder = problem_mod.ReportBuilder.init(self.gpa, self.module_env, self.module_env, &self.checker.snapshots, "test", &.{}, &self.checker.import_mapping); + defer report_builder.deinit(); + + var report_buf = try std.array_list.Managed(u8).initCapacity(self.gpa, 256); + defer report_buf.deinit(); + + for (self.checker.problems.problems.items) |problem| { + var report = try report_builder.build(problem); + defer report.deinit(); + + try renderReportToMarkdownBuffer(&report_buf, &report); + try testing.expectEqualStrings("EXPECTED NO ERROR", report_buf.items); + } + + try testing.expectEqual(0, self.checker.problems.problems.items.len); +} diff --git a/src/check/test/builtin_scope_test.zig b/src/check/test/builtin_scope_test.zig new file mode 100644 index 0000000000..cf9bbf09dd --- /dev/null +++ b/src/check/test/builtin_scope_test.zig @@ -0,0 +1,98 @@ +//! Tests verifying that "Builtin" is not in scope and cannot be imported, +//! but that nested types like Str, List, etc. are available. + +const TestEnv = @import("./TestEnv.zig"); +const testing = @import("std").testing; +const std = @import("std"); + +test "cannot import Builtin module" { + const src = + \\import Builtin + \\ + \\x = 5 + ; + + var test_env = try TestEnv.init("Test", src); + defer test_env.deinit(); + + // Should have a canonicalization problem because Builtin is not a module that can be imported + const diagnostics = try test_env.module_env.getDiagnostics(); + defer test_env.module_env.gpa.free(diagnostics); + + // Expect at least one diagnostic (module not found error) + try testing.expect(diagnostics.len > 0); +} + +test "can define userspace type named Builtin" { + const src = + \\Test := [A, B, C] + \\ + \\Builtin := [D, E, F] + \\ + \\x : Builtin + \\x = D + ; + + var test_env = try TestEnv.init("Test", src); + defer test_env.deinit(); + + // Should have no problems - Builtin is a valid userspace name + try test_env.assertDefType("x", "Builtin"); +} + +test "builtin types are still available without import" { + const src = + \\Test := [Whatever] + \\ + \\x : Str + \\x = "hello" + \\ + \\y : List(U64) + \\y = [1, 2, 3] + ; + + var test_env = try TestEnv.init("Test", src); + defer test_env.deinit(); + + // Builtin types like Str and List should work without importing Builtin + try test_env.assertDefType("x", "Str"); + try test_env.assertDefType("y", "List(U64)"); +} + +test "can import userspace Builtin module" { + const builtin_module_src = + \\Builtin := [D, E, F] + \\ + \\value : Builtin + \\value = D + ; + + var builtin_module = try TestEnv.init("Builtin", builtin_module_src); + defer builtin_module.deinit(); + + const main_src = + \\Main := [Whatever] + \\ + \\import Builtin + \\ + \\x : Builtin + \\x = Builtin.value + ; + + var main_module = try TestEnv.initWithImport("Main", main_src, "Builtin", &builtin_module); + defer main_module.deinit(); + + // Should successfully import the userspace Builtin module without "module not found" error + const diagnostics = try main_module.module_env.getDiagnostics(); + defer main_module.module_env.gpa.free(diagnostics); + + // Check that there's no "module not found" error for "Builtin" + for (diagnostics) |diag| { + if (diag == .module_not_found) { + const module_name = main_module.module_env.getIdent(diag.module_not_found.module_name); + if (std.mem.eql(u8, module_name, "Builtin")) { + try testing.expect(false); // Should not have module_not_found for Builtin + } + } + } +} diff --git a/src/check/test/cross_module_test.zig b/src/check/test/cross_module_test.zig index ab53d0d207..b1398b2c2e 100644 --- a/src/check/test/cross_module_test.zig +++ b/src/check/test/cross_module_test.zig @@ -5,6 +5,7 @@ const base = @import("base"); const types_mod = @import("types"); const can = @import("can"); const Check = @import("../Check.zig"); +const TestEnv = @import("./TestEnv.zig"); const CIR = can.CIR; const Var = types_mod.Var; @@ -21,1415 +22,201 @@ const UnifierScratch = @import("../unify.zig").Scratch; const OccursScratch = occurs.Scratch; const unify = @import("../unify.zig").unify; -test "cross-module type checking - monomorphic function" { - const allocator = testing.allocator; +test "cross-module - check type - monomorphic function passes" { + const source_a = + \\main! : Str -> Str + \\main! = |s| s + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + // Str is auto-imported from Builtin module, so it prints as "Str" + try test_env_a.assertLastDefType("Str -> Str"); - // Create module A that exports a simple function - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); - - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; - - const i32_content = Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }; - const arg1_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), i32_content, base.Region.zero(), Var); - const arg2_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), i32_content, base.Region.zero(), Var); - const ret_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), i32_content, base.Region.zero(), Var); - - const func_content = try module_a_env.types.mkFuncPure(&[_]Var{ arg1_var, arg2_var }, ret_var); - const func_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, func_content, base.Region.zero()); - - // Set the type of expression 0 (which maps to var 0) - // Type is already set by addExprAndTypeVar - - // Create array of module environments - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - - try modules.append(module_a_cir); - - // Create module B that imports and uses the function from module A - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Register the import of module A - _ = try module_b_cir.imports.getOrPut(allocator, &module_b_cir.common.strings, "ModuleA"); - - // Create an external lookup expression - const external_lookup_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), // Direct index to module A in the array - .target_node_idx = @intCast(@intFromEnum(func_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - try modules.append(module_b_cir); - - // Type check module B - var checker = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker.deinit(); - - _ = try checker.checkExpr(external_lookup_expr); - - // Verify that the external lookup has the correct type - const external_var = @as(Var, @enumFromInt(@intFromEnum(external_lookup_expr))); - const resolved = module_b_env.types.resolveVar(external_var); - - try testing.expect(resolved.desc.content == .structure); - try testing.expect(resolved.desc.content.structure == .fn_pure); - - const func = resolved.desc.content.structure.fn_pure; - const args = module_b_env.types.sliceVars(func.args); - try testing.expectEqual(@as(usize, 2), args.len); - - // Check that all arguments and return are I32 - for (args) |arg| { - const arg_resolved = module_b_env.types.resolveVar(arg); - try testing.expect(arg_resolved.desc.content == .structure); - try testing.expect(arg_resolved.desc.content.structure == .num); - try testing.expect(arg_resolved.desc.content.structure.num == .int_precision); - try testing.expectEqual(types_mod.Num.Int.Precision.i32, arg_resolved.desc.content.structure.num.int_precision); - } - - const ret_resolved = module_b_env.types.resolveVar(func.ret); - try testing.expect(ret_resolved.desc.content == .structure); - try testing.expect(ret_resolved.desc.content.structure == .num); - try testing.expect(ret_resolved.desc.content.structure.num == .int_precision); - try testing.expectEqual(types_mod.Num.Int.Precision.i32, ret_resolved.desc.content.structure.num.int_precision); + const source_b = + \\import A + \\ + \\main : Str + \\main = A.main!("hello") + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + // Str is auto-imported from Builtin module, so it prints as "Str" + try test_env_b.assertLastDefType("Str"); } -test "cross-module type checking - polymorphic function" { - const allocator = testing.allocator; +test "cross-module - check type - monomorphic function fails" { + const source_a = + \\main! : Str -> Str + \\main! = |s| s + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + try test_env_a.assertLastDefType("Str -> Str"); - // Create module A that exports a polymorphic identity function - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); - - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; - - // Create a function: identity : a -> a - const type_var_a = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - const func_content = try module_a_env.types.mkFuncPure(&[_]Var{type_var_a}, type_var_a); - const func_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, func_content, base.Region.zero()); - - // Create array of module environments - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - - try modules.append(module_a_cir); - - // Create module B that imports and uses the polymorphic function - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Register the import of module A - _ = try module_b_cir.imports.getOrPut(allocator, &module_b_cir.common.strings, "ModuleA"); - - // Create an external lookup expression - const external_lookup_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), // Direct index to module A in the array - .target_node_idx = @intCast(@intFromEnum(func_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - try modules.append(module_b_cir); - - // Type check module B - var checker = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker.deinit(); - - _ = try checker.checkExpr(external_lookup_expr); - - // Verify that the external lookup has a polymorphic type - const external_var = @as(Var, @enumFromInt(@intFromEnum(external_lookup_expr))); - const resolved = module_b_env.types.resolveVar(external_var); - - try testing.expect(resolved.desc.content == .structure); - try testing.expect(resolved.desc.content.structure == .fn_pure); - - const func = resolved.desc.content.structure.fn_pure; - const args = module_b_env.types.sliceVars(func.args); - try testing.expectEqual(@as(usize, 1), args.len); - - // The function should still be polymorphic (flex var) - const arg_resolved = module_b_env.types.resolveVar(args[0]); - const ret_resolved = module_b_env.types.resolveVar(func.ret); - - // Both should resolve to the same flex var - try testing.expectEqual(arg_resolved.var_, ret_resolved.var_); - try testing.expect(arg_resolved.desc.content == .flex_var); + const source_b = + \\import A + \\ + \\main : U8 + \\main = A.main!(1) + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertOneTypeError("TYPE MISMATCH"); } -test "cross-module type checking - record type" { - const allocator = testing.allocator; +test "cross-module - check type - polymorphic function passes" { + const source_a = + \\main! : a -> a + \\main! = |s| s + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + try test_env_a.assertLastDefType("a -> a"); - // Create module A that exports a record type - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); - - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; - - // Create a record type { x: I32, y: Str } - var record_fields = std.ArrayList(types_mod.RecordField).init(allocator); - defer record_fields.deinit(); - - const x_ident = try module_a_env.insertIdent(base.Ident.for_text("x")); - const y_ident = try module_a_env.insertIdent(base.Ident.for_text("y")); - - const i32_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }, base.Region.zero(), Var); - const str_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .structure = .str }, base.Region.zero(), Var); - - try record_fields.append(.{ .name = x_ident, .var_ = i32_var }); - try record_fields.append(.{ .name = y_ident, .var_ = str_var }); - - const fields_range = try module_a_env.types.appendRecordFields(record_fields.items); - const ext_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .structure = .empty_record }, base.Region.zero(), Var); - - const record_content = Content{ - .structure = .{ - .record = .{ - .fields = fields_range, - .ext = ext_var, - }, - }, - }; - - // Set the type of this expression to our record type - const record_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, record_content, base.Region.zero()); - - // Create array of module environments - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - - try modules.append(module_a_cir); - - // Create module B that imports and uses the record from module A - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Register the import of module A - _ = try module_b_cir.imports.getOrPut(allocator, &module_b_cir.common.strings, "ModuleA"); - - // Create an external lookup expression - const external_lookup_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), // Direct index to module A in the array - .target_node_idx = @intCast(@intFromEnum(record_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - try modules.append(module_b_cir); - - // Type check module B - var checker = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker.deinit(); - - _ = try checker.checkExpr(external_lookup_expr); - - // Verify that the external lookup has the correct record type - // Verify the record type - const external_var = @as(Var, @enumFromInt(@intFromEnum(external_lookup_expr))); - const resolved = module_b_env.types.resolveVar(external_var); - - try testing.expect(resolved.desc.content == .structure); - try testing.expect(resolved.desc.content.structure == .record); - - const record = resolved.desc.content.structure.record; - const fields = module_b_env.types.getRecordFieldsSlice(record.fields); - try testing.expectEqual(@as(usize, 2), fields.len); - - // Check field names and types - try testing.expectEqual(x_ident, fields.items(.name)[0]); - try testing.expectEqual(y_ident, fields.items(.name)[1]); - - const x_resolved = module_b_env.types.resolveVar(fields.items(.var_)[0]); - try testing.expect(x_resolved.desc.content == .structure); - try testing.expect(x_resolved.desc.content.structure == .num); - try testing.expect(x_resolved.desc.content.structure.num == .int_precision); - try testing.expectEqual(types_mod.Num.Int.Precision.i32, x_resolved.desc.content.structure.num.int_precision); - - // Check field y is Str - const y_resolved = module_b_env.types.resolveVar(fields.items(.var_)[1]); - try testing.expect(y_resolved.desc.content == .structure); - try testing.expect(y_resolved.desc.content.structure == .str); + const source_b = + \\import A + \\ + \\main : Str + \\main = A.main!("hello") + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + // Str is auto-imported from Builtin module, so it prints as "Str" + try test_env_b.assertLastDefType("Str"); } -test "cross-module type checking - type mismatch error" { - const allocator = testing.allocator; +test "cross-module - check type - polymorphic function with multiple uses passes" { + const source_a = + \\main! : a -> a + \\main! = |s| s + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + try test_env_a.assertLastDefType("a -> a"); - // Create module A that exports an I32 - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); - - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; - - // Create an I32 type - const i32_content = Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }; - - // Store this as an integer literal expression - const func_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{ 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, .kind = .i128 }, - } }, i32_content, base.Region.zero()); - - // Create module B that imports the I32 but tries to use it as a String - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Register the import of module A - _ = try module_b_cir.imports.getOrPut(allocator, &module_b_cir.common.strings, "ModuleA"); - - // Create an external lookup expression - const external_lookup_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), // Direct index to module A in the array - .target_node_idx = @intCast(@intFromEnum(func_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Create a string literal expression - const str_content = Content{ .structure = .str }; - const str_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_str_segment = .{ - .literal = @enumFromInt(0), // placeholder string literal - }, - }, str_content, base.Region.zero()); - - // Create array of module environments - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - - try modules.append(module_a_cir); - try modules.append(module_b_cir); - - // Type check module B - var checker = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker.deinit(); - - _ = try checker.checkExpr(external_lookup_expr); - _ = try checker.checkExpr(str_expr); - - // Try to unify the imported I32 with a String - this should fail - const external_var = @as(Var, @enumFromInt(@intFromEnum(external_lookup_expr))); - const string_var = @as(Var, @enumFromInt(@intFromEnum(str_expr))); - - const result = try checker.unify(external_var, string_var); - try testing.expect(result.isProblem()); + const source_b = + \\import A + \\ + \\main : U64 + \\main = { + \\ a = A.main!(10) + \\ b = A.main!(15) + \\ _c = A.main!("Hello") + \\ a + b + \\} + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertLastDefType("U64"); } -test "cross-module type checking - polymorphic instantiation" { - const allocator = testing.allocator; +test "cross-module - check type - static dispatch" { + const source_a = + \\A := [A(Str)].{ + \\ to_str : A -> Str + \\ to_str = |A.A(val)| val + \\} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + try test_env_a.assertDefType("A.to_str", "A -> Str"); - // Create module A that exports a polymorphic list function - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); - - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; - - // Store this as an integer literal expression (placeholder) - const type_var_a = try module_a_cir.addTypeSlotAndTypeVar( - @enumFromInt(0), - Content{ .flex_var = null }, - base.Region.zero(), - types_mod.Var, - ); - const list_var = try module_a_cir.addTypeSlotAndTypeVar( - @enumFromInt(0), - Content{ .structure = .{ .list = type_var_a } }, - base.Region.zero(), - types_mod.Var, - ); - const i64_var = try module_a_cir.addTypeSlotAndTypeVar( - @enumFromInt(0), - Content{ .structure = .{ .num = .{ .int_precision = .i64 } } }, - base.Region.zero(), - types_mod.Var, - ); - const func_content = try module_a_env.types.mkFuncPure(&[_]Var{list_var}, i64_var); - const func_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, func_content, base.Region.zero()); - - // Create module B that imports and uses the function with a specific type - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Register the import of module A - _ = try module_b_cir.imports.getOrPut(allocator, &module_b_cir.common.strings, "ModuleA"); - - // Create an external lookup expression - const external_lookup_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), // Direct index to module A in the array - .target_node_idx = @intCast(@intFromEnum(func_expr_idx)), - .region = base.Region.zero(), - }, - }, .{ .flex_var = null }, base.Region.zero()); - - // Create an empty list expression - const str_var = try module_b_cir.addTypeSlotAndTypeVar( - @enumFromInt(0), - Content{ .structure = .str }, - base.Region.zero(), - types_mod.Var, - ); - const list_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_empty_list = .{}, - }, Content{ .structure = .{ .list = str_var } }, base.Region.zero()); - - // Create array of module environments - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - - try modules.append(module_a_cir); - try modules.append(module_b_cir); - - // Type check module B - var checker = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker.deinit(); - - _ = try checker.checkExpr(external_lookup_expr); - _ = try checker.checkExpr(list_expr); - - // The polymorphic function should be usable with List(Str) - const external_var = @as(Var, @enumFromInt(@intFromEnum(external_lookup_expr))); - const resolved = module_b_env.types.resolveVar(external_var); - - try testing.expect(resolved.desc.content == .structure); - try testing.expect(resolved.desc.content.structure == .fn_pure); - - // The function should accept any list type - const func = resolved.desc.content.structure.fn_pure; - const args = module_b_env.types.sliceVars(func.args); - try testing.expectEqual(@as(usize, 1), args.len); - - // The argument should be a list with a flex var - const arg_resolved = module_b_env.types.resolveVar(args[0]); - try testing.expect(arg_resolved.desc.content == .structure); - try testing.expect(arg_resolved.desc.content.structure == .list); + const source_b = + \\import A + \\ + \\a_val = A.A("hello") + \\ + \\main = a_val.to_str() + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertDefType("a_val", "A"); + try test_env_b.assertDefType("main", "Str"); } -test "cross-module type checking - preserves module A types" { - const allocator = testing.allocator; +test "cross-module - check type - static dispatch - no annotation & indirection" { + const source_a = + \\A := [A(Str)].{ + \\ to_str = |A.A(val)| val + \\ to_str2 = |x| x.to_str() + \\} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + try test_env_a.assertDefType("A.to_str", "A -> Str"); + try test_env_a.assertDefType("A.to_str2", "a -> b where [a.to_str : a -> b]"); - // Create module A - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); - - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; - - // Create a flex var in module A - const flex_var_a = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - - // Store this as an integer literal expression (placeholder) - const flex_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, Content{ .flex_var = null }, base.Region.zero()); - - // Remember the original state - const original_content = module_a_env.types.resolveVar(flex_var_a).desc.content; - try testing.expect(original_content == .flex_var); - - // Create module B - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Register the import of module A - _ = try module_b_cir.imports.getOrPut(allocator, &module_b_cir.common.strings, "ModuleA"); - - // Create an external lookup expression - const external_lookup_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), // Direct index to module A in the array - .target_node_idx = @intCast(@intFromEnum(flex_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Create a concrete type in module B to unify with - const i32_var = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }, base.Region.zero(), Var); - const i32_expr = try module_b_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{ 123, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, .kind = .i128 }, - } }, Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }, base.Region.zero()); - - // Create array of module environments - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - - try modules.append(module_a_cir); - try modules.append(module_b_cir); - - // Type check module B - var checker = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker.deinit(); - - _ = try checker.checkExpr(external_lookup_expr); - _ = try checker.checkExpr(i32_expr); - - // Unify the imported type with I32 - const external_var = @as(Var, @enumFromInt(@intFromEnum(external_lookup_expr))); - const result = try checker.unify(external_var, i32_var); - try testing.expect(result.isOk()); - - // Module A's type should remain unchanged (still a flex var) - const module_a_after = module_a_env.types.resolveVar(flex_var_a).desc.content; - try testing.expect(module_a_after == .flex_var); - try testing.expectEqual(original_content, module_a_after); - - // Module B's imported type should be I32 - const module_b_resolved = module_b_env.types.resolveVar(external_var); - try testing.expect(module_b_resolved.desc.content == .structure); - try testing.expect(module_b_resolved.desc.content.structure == .num); - try testing.expect(module_b_resolved.desc.content.structure.num == .int_precision); - try testing.expectEqual(types_mod.Num.Int.Precision.i32, module_b_resolved.desc.content.structure.num.int_precision); + const source_b = + \\import A + \\ + \\val1 = A.A("hello") + \\val2 = A.A("world") + \\ + \\main = (val1.to_str(), val1.to_str2(), val2.to_str2()) + \\ + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertDefType("val1", "A"); + try test_env_b.assertDefType("val2", "A"); + try test_env_b.assertDefType("main", "(Str, Str, Str)"); } -test "cross-module type checking - three module chain monomorphic" { - const allocator = testing.allocator; +test "cross-module - check type - opaque types 1" { + const source_a = + \\A :: [A(Str)].{} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); - // Module A exports a simple function: add : I32, I32 -> I32 - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); - - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; - - // Create a function: add : I32, I32 -> I32 - const i32_content = Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }; - const arg1_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), i32_content, base.Region.zero(), Var); - const arg2_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), i32_content, base.Region.zero(), Var); - const ret_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), i32_content, base.Region.zero(), Var); - - const func_content = try module_a_env.types.mkFuncPure(&[_]Var{ arg1_var, arg2_var }, ret_var); - const func_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, func_content, base.Region.zero()); - - // Module B imports from A and re-exports it - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Module B imports A's function - _ = try module_b_cir.imports.getOrPut(allocator, &module_b_cir.common.strings, "ModuleA"); - - const b_lookup_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), // Direct index to module A in the array - .target_node_idx = @intCast(@intFromEnum(func_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Module C imports from B - var module_c_env = try ModuleEnv.init(allocator, ""); - defer module_c_env.deinit(); - - try module_c_env.initCIRFields(allocator, "ModuleC"); - const module_c_cir = &module_c_env; - - // Module C imports from B (not A) - _ = try module_c_cir.imports.getOrPut(allocator, &module_c_cir.common.strings, "ModuleB"); - const c_lookup_expr = try module_c_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(1), // Direct index to module B in the array - .target_node_idx = @intCast(@intFromEnum(b_lookup_expr)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Create array of module environments - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - - try modules.append(module_a_cir); - try modules.append(module_b_cir); - try modules.append(module_c_cir); - - // Type check module B - var checker_b = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker_b.deinit(); - - _ = try checker_b.checkExpr(b_lookup_expr); - - // Type check module C - var checker_c = try Check.init(allocator, &module_c_env.types, module_c_cir, modules.items, &module_c_cir.store.regions); - defer checker_c.deinit(); - - _ = try checker_c.checkExpr(c_lookup_expr); - - // Verify that module C sees the correct type through the chain - const c_var = @as(Var, @enumFromInt(@intFromEnum(c_lookup_expr))); - const c_resolved = module_c_env.types.resolveVar(c_var); - - try testing.expect(c_resolved.desc.content == .structure); - try testing.expect(c_resolved.desc.content.structure == .fn_pure); - - const func = c_resolved.desc.content.structure.fn_pure; - const args = module_c_env.types.sliceVars(func.args); - try testing.expectEqual(@as(usize, 2), args.len); - - // Check that all arguments and return are I32 - for (args) |arg| { - const arg_resolved = module_c_env.types.resolveVar(arg); - try testing.expect(arg_resolved.desc.content == .structure); - try testing.expect(arg_resolved.desc.content.structure == .num); - try testing.expect(arg_resolved.desc.content.structure.num == .int_precision); - try testing.expectEqual(types_mod.Num.Int.Precision.i32, arg_resolved.desc.content.structure.num.int_precision); - } - - const ret_resolved = module_c_env.types.resolveVar(func.ret); - try testing.expect(ret_resolved.desc.content == .structure); - try testing.expect(ret_resolved.desc.content.structure == .num); - try testing.expect(ret_resolved.desc.content.structure.num == .int_precision); - try testing.expectEqual(types_mod.Num.Int.Precision.i32, ret_resolved.desc.content.structure.num.int_precision); + const source_b = + \\import A + \\ + \\a_val : A.A + \\a_val = A("hello") + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertFirstTypeError("TYPE MISMATCH"); } -test "cross-module type checking - three module chain polymorphic" { - const allocator = testing.allocator; +test "cross-module - check type - opaque types 2" { + const source_a = + \\A :: [A(Str)].{} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); - // Module A exports a polymorphic function: identity : a -> a - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); - - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; - - const type_var_a = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - const func_content = try module_a_env.types.mkFuncPure(&[_]Var{type_var_a}, type_var_a); - const func_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, func_content, base.Region.zero()); - - // Module B imports from A and re-exports it - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Module B imports from A - _ = try module_b_cir.imports.getOrPut(allocator, &module_b_cir.common.strings, "ModuleA"); - const b_lookup_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), // Direct index to module A in the array - .target_node_idx = @intCast(@intFromEnum(func_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Module C imports from B - var module_c_env = try ModuleEnv.init(allocator, ""); - defer module_c_env.deinit(); - - try module_c_env.initCIRFields(allocator, "ModuleC"); - const module_c_cir = &module_c_env; - - // Module C imports from B - _ = try module_c_cir.imports.getOrPut(allocator, &module_c_cir.common.strings, "ModuleB"); - const c_lookup_expr = try module_c_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(1), // Direct index to module B in the array - .target_node_idx = @intCast(@intFromEnum(b_lookup_expr)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Create array of module environments - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - - try modules.append(module_a_cir); - try modules.append(module_b_cir); - try modules.append(module_c_cir); - - // Type check module B - var checker_b = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker_b.deinit(); - - _ = try checker_b.checkExpr(b_lookup_expr); - - // Type check module C - var checker_c = try Check.init(allocator, &module_c_env.types, module_c_cir, modules.items, &module_c_cir.store.regions); - defer checker_c.deinit(); - - _ = try checker_c.checkExpr(c_lookup_expr); - - // Verify that module C sees the polymorphic type through the chain - const c_var = @as(Var, @enumFromInt(@intFromEnum(c_lookup_expr))); - const c_resolved = module_c_env.types.resolveVar(c_var); - - try testing.expect(c_resolved.desc.content == .structure); - try testing.expect(c_resolved.desc.content.structure == .fn_pure); - - const func = c_resolved.desc.content.structure.fn_pure; - const args = module_c_env.types.sliceVars(func.args); - try testing.expectEqual(@as(usize, 1), args.len); - - // The function should still be polymorphic - const arg_resolved = module_c_env.types.resolveVar(args[0]); - const ret_resolved = module_c_env.types.resolveVar(func.ret); - - // Both should resolve to the same flex var - try testing.expectEqual(arg_resolved.var_, ret_resolved.var_); - try testing.expect(arg_resolved.desc.content == .flex_var); + const source_b = + \\import A + \\ + \\a_val = A.A("hello") + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertFirstTypeError("CANNOT USE OPAQUE NOMINAL TYPE"); } -test "cross-module type checking - partial polymorphic instantiation chain" { - const allocator = testing.allocator; +test "displayNameIsBetter - shorter names are preferred" { + // Tests the core comparison logic used when multiple imports provide different + // display names for the same type (e.g., `import Foo as F` and `import Foo as Foo`). + // The shortest name wins for error message display. For equal lengths, the + // lexicographically smaller name wins (deterministic regardless of import order). + const displayNameIsBetter = Check.displayNameIsBetter; - // Module A exports: map : (a -> b), List a -> List b - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); + // Shorter is better + try testing.expect(displayNameIsBetter("T", "Type")); + try testing.expect(displayNameIsBetter("AB", "ABC")); + try testing.expect(!displayNameIsBetter("Type", "T")); + try testing.expect(!displayNameIsBetter("ABC", "AB")); - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; + // Equal length: lexicographically smaller wins + try testing.expect(displayNameIsBetter("Abc", "Bbc")); // 'A' < 'B' + try testing.expect(displayNameIsBetter("Aac", "Abc")); // 'a' < 'b' at position 1 + try testing.expect(!displayNameIsBetter("Bbc", "Abc")); + try testing.expect(!displayNameIsBetter("Abc", "Aac")); - // Module A exports: map : (a -> b), List a -> List b - - // Create (a -> b) - const type_var_a = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - const type_var_b = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - const mapper_func_content = try module_a_env.types.mkFuncPure(&[_]Var{type_var_a}, type_var_b); - const mapper_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), mapper_func_content, base.Region.zero(), Var); - - // Create List a - const list_a_content = Content{ .structure = .{ .list = type_var_a } }; - const list_a_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), list_a_content, base.Region.zero(), Var); - - // Create List b - const list_b_content = Content{ .structure = .{ .list = type_var_b } }; - const list_b_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), list_b_content, base.Region.zero(), Var); - - // Create map : (a -> b), List a -> List b - const map_func_content = try module_a_env.types.mkFuncPure(&[_]Var{ mapper_var, list_a_var }, list_b_var); - - // Update the expression with the correct function type - const map_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, map_func_content, base.Region.zero()); - - // Module B imports map and partially applies it with I32 - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Module B imports from A - _ = try module_b_cir.imports.getOrPut(allocator, &module_b_cir.common.strings, "ModuleA"); - const b_lookup_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), // Direct index to module A in the array - .target_node_idx = @intCast(@intFromEnum(map_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Create specialized version: mapI32 : (I32 -> b), List I32 -> List b - - const i32_content = Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }; - const i32_var = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), i32_content, base.Region.zero(), Var); - const b_type_var_b = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - - const i32_to_b_content = try module_b_env.types.mkFuncPure(&[_]Var{i32_var}, b_type_var_b); - const i32_to_b_var = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), i32_to_b_content, base.Region.zero(), Var); - - const list_i32_content = Content{ .structure = .{ .list = i32_var } }; - const list_i32_var = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), list_i32_content, base.Region.zero(), Var); - - const list_b_content_2 = Content{ .structure = .{ .list = b_type_var_b } }; - const list_b_var_2 = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), list_b_content_2, base.Region.zero(), Var); - - const map_i32_content = try module_b_env.types.mkFuncPure(&[_]Var{ i32_to_b_var, list_i32_var }, list_b_var_2); - - const map_i32_expr_idx = try module_b_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, map_i32_content, base.Region.zero()); - - // Module C imports the partially specialized version from B - var module_c_env = try ModuleEnv.init(allocator, ""); - defer module_c_env.deinit(); - - try module_c_env.initCIRFields(allocator, "ModuleC"); - const module_c_cir = &module_c_env; - - // Module C imports from B and uses the partially specialized function - _ = try module_c_cir.imports.getOrPut(allocator, &module_c_cir.common.strings, "ModuleB"); - const c_lookup_expr = try module_c_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(1), // Direct index to module B in the array - .target_node_idx = @intCast(@intFromEnum(map_i32_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Create array of module environments - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - - try modules.append(module_a_cir); - try modules.append(module_b_cir); - try modules.append(module_c_cir); - - // Type check module B - var checker_b = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker_b.deinit(); - - _ = try checker_b.checkExpr(b_lookup_expr); - _ = try checker_b.checkExpr(map_i32_expr_idx); - - // Type check module C - var checker_c = try Check.init(allocator, &module_c_env.types, module_c_cir, modules.items, &module_c_cir.store.regions); - defer checker_c.deinit(); - - _ = try checker_c.checkExpr(c_lookup_expr); - - // Verify that module C sees the partially specialized type - const c_var = @as(Var, @enumFromInt(@intFromEnum(c_lookup_expr))); - const c_resolved = module_c_env.types.resolveVar(c_var); - - try testing.expect(c_resolved.desc.content == .structure); - try testing.expect(c_resolved.desc.content.structure == .fn_pure); - - const func = c_resolved.desc.content.structure.fn_pure; - const args = module_c_env.types.sliceVars(func.args); - try testing.expectEqual(@as(usize, 2), args.len); - - // First argument should be (I32 -> b) - const mapper_resolved = module_c_env.types.resolveVar(args[0]); - try testing.expect(mapper_resolved.desc.content == .structure); - try testing.expect(mapper_resolved.desc.content.structure == .fn_pure); - - const mapper_func = mapper_resolved.desc.content.structure.fn_pure; - const mapper_args = module_c_env.types.sliceVars(mapper_func.args); - try testing.expectEqual(@as(usize, 1), mapper_args.len); - - // The mapper input should be I32 - const mapper_input_resolved = module_c_env.types.resolveVar(mapper_args[0]); - try testing.expect(mapper_input_resolved.desc.content == .structure); - try testing.expect(mapper_input_resolved.desc.content.structure == .num); - try testing.expect(mapper_input_resolved.desc.content.structure.num == .int_precision); - try testing.expectEqual(types_mod.Num.Int.Precision.i32, mapper_input_resolved.desc.content.structure.num.int_precision); - - // The mapper output should still be polymorphic - const mapper_output_resolved = module_c_env.types.resolveVar(mapper_func.ret); - try testing.expect(mapper_output_resolved.desc.content == .flex_var); - - // Second argument should be List I32 - const list_arg_resolved = module_c_env.types.resolveVar(args[1]); - try testing.expect(list_arg_resolved.desc.content == .structure); - try testing.expect(list_arg_resolved.desc.content.structure == .list); - - const list_elem_var = list_arg_resolved.desc.content.structure.list; - const list_elem_resolved = module_c_env.types.resolveVar(list_elem_var); - try testing.expect(list_elem_resolved.desc.content == .structure); - try testing.expect(list_elem_resolved.desc.content.structure == .num); - try testing.expect(list_elem_resolved.desc.content.structure.num == .int_precision); - try testing.expectEqual(types_mod.Num.Int.Precision.i32, list_elem_resolved.desc.content.structure.num.int_precision); - - // Return type should be List b (still polymorphic) - const ret_resolved = module_c_env.types.resolveVar(func.ret); - try testing.expect(ret_resolved.desc.content == .structure); - try testing.expect(ret_resolved.desc.content.structure == .list); - - const ret_elem_var = ret_resolved.desc.content.structure.list; - const ret_elem_resolved = module_c_env.types.resolveVar(ret_elem_var); - try testing.expect(ret_elem_resolved.desc.content == .flex_var); - - // The output type variable should match the mapper's output - try testing.expectEqual(mapper_output_resolved.var_, ret_elem_resolved.var_); -} - -test "cross-module type checking - record type chain" { - const allocator = testing.allocator; - - // Module A exports a record type: { x: I32, y: Str } - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); - - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; - - // Module A exports a record type: { x: I32, y: Str } - const i32_content = Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }; - const i32_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), i32_content, base.Region.zero(), Var); - - const str_content = Content{ .structure = .str }; - const str_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), str_content, base.Region.zero(), Var); - - const x_ident = try module_a_env.insertIdent(base.Ident.for_text("x")); - const y_ident = try module_a_env.insertIdent(base.Ident.for_text("y")); - - var record_fields = std.ArrayList(types_mod.RecordField).init(allocator); - defer record_fields.deinit(); - - try record_fields.append(.{ .name = x_ident, .var_ = i32_var }); - try record_fields.append(.{ .name = y_ident, .var_ = str_var }); - - const fields_range = try module_a_env.types.appendRecordFields(record_fields.items); - const ext_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - - const record_content = Content{ .structure = .{ .record = .{ .fields = fields_range, .ext = ext_var } } }; - - const record_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, record_content, base.Region.zero()); - - // Module B imports and re-exports the record - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Module B imports from A and partially specializes - _ = try module_b_cir.imports.getOrPut(allocator, &module_b_cir.common.strings, "ModuleA"); - const b_lookup_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), // Direct index to module A in the array - .target_node_idx = @intCast(@intFromEnum(record_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Module C imports from B - var module_c_env = try ModuleEnv.init(allocator, ""); - defer module_c_env.deinit(); - - try module_c_env.initCIRFields(allocator, "ModuleC"); - const module_c_cir = &module_c_env; - - // Module C imports from B - _ = try module_c_cir.imports.getOrPut(allocator, &module_c_cir.common.strings, "ModuleB"); - const c_lookup_expr = try module_c_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(1), // Direct index to module B in the array - .target_node_idx = @intCast(@intFromEnum(b_lookup_expr)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Create array of module environments - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - - try modules.append(module_a_cir); - try modules.append(module_b_cir); - try modules.append(module_c_cir); - - // Type check module B - var checker_b = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker_b.deinit(); - - _ = try checker_b.checkExpr(b_lookup_expr); - - // Type check module C - var checker_c = try Check.init(allocator, &module_c_env.types, module_c_cir, modules.items, &module_c_cir.store.regions); - defer checker_c.deinit(); - - _ = try checker_c.checkExpr(c_lookup_expr); - - // Verify the record type in module C - const c_var = @as(Var, @enumFromInt(@intFromEnum(c_lookup_expr))); - const c_resolved = module_c_env.types.resolveVar(c_var); - - try testing.expect(c_resolved.desc.content == .structure); - try testing.expect(c_resolved.desc.content.structure == .record); - - const record = c_resolved.desc.content.structure.record; - const fields = module_c_env.types.getRecordFieldsSlice(record.fields); - try testing.expectEqual(@as(usize, 2), fields.len); - - // Check field names and types - try testing.expectEqualSlices(u8, "x", module_c_env.getIdent(fields.items(.name)[0])); - try testing.expectEqualSlices(u8, "y", module_c_env.getIdent(fields.items(.name)[1])); - - // Check field x is I32 - const x_resolved = module_c_env.types.resolveVar(fields.items(.var_)[0]); - try testing.expect(x_resolved.desc.content == .structure); - try testing.expect(x_resolved.desc.content.structure == .num); - try testing.expect(x_resolved.desc.content.structure.num == .int_precision); - try testing.expectEqual(types_mod.Num.Int.Precision.i32, x_resolved.desc.content.structure.num.int_precision); - - // Check field y is Str - const y_resolved = module_c_env.types.resolveVar(fields.items(.var_)[1]); - try testing.expect(y_resolved.desc.content == .structure); - try testing.expect(y_resolved.desc.content.structure == .str); -} - -test "cross-module type checking - polymorphic record chain" { - const allocator = testing.allocator; - - // Module A exports a polymorphic record: { value: a, next: List a } - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); - - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; - - // Module A exports a polymorphic record: { value: a, next: List a } - const type_var_a = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - - const list_a_content = Content{ .structure = .{ .list = type_var_a } }; - const list_a_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), list_a_content, base.Region.zero(), Var); - - const value_ident = try module_a_env.insertIdent(base.Ident.for_text("value")); - const next_ident = try module_a_env.insertIdent(base.Ident.for_text("next")); - - var record_fields = std.ArrayList(types_mod.RecordField).init(allocator); - defer record_fields.deinit(); - - try record_fields.append(.{ .name = value_ident, .var_ = type_var_a }); - try record_fields.append(.{ .name = next_ident, .var_ = list_a_var }); - - const fields_range = try module_a_env.types.appendRecordFields(record_fields.items); - const ext_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - - const record_content = Content{ .structure = .{ .record = .{ .fields = fields_range, .ext = ext_var } } }; - - const record_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, record_content, base.Region.zero()); - - // Module B imports and partially specializes to Str - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Module B imports from A - _ = try module_b_cir.imports.getOrPut(allocator, &module_b_cir.common.strings, "ModuleA"); - const b_lookup_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), // Direct index to module A in the array - .target_node_idx = @intCast(@intFromEnum(record_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Create specialized version: { value: Str, next: List Str } - const str_content = Content{ .structure = .str }; - const str_var = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), str_content, base.Region.zero(), Var); - - const list_str_content = Content{ .structure = .{ .list = str_var } }; - const list_str_var = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), list_str_content, base.Region.zero(), Var); - - const value_ident_b = try module_b_env.insertIdent(base.Ident.for_text("value")); - const next_ident_b = try module_b_env.insertIdent(base.Ident.for_text("next")); - - var str_record_fields = std.ArrayList(types_mod.RecordField).init(allocator); - defer str_record_fields.deinit(); - - try str_record_fields.append(.{ .name = value_ident_b, .var_ = str_var }); - try str_record_fields.append(.{ .name = next_ident_b, .var_ = list_str_var }); - - const str_fields_range = try module_b_env.types.appendRecordFields(str_record_fields.items); - const str_ext_var = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .structure = .empty_record }, base.Region.zero(), Var); - - const str_record_content = Content{ .structure = .{ .record = .{ .fields = str_fields_range, .ext = str_ext_var } } }; - - // Set the type on var 1 (for expr 1) - const str_record_expr_idx = try module_b_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, str_record_content, base.Region.zero()); - - // Module C imports the specialized version from B - var module_c_env = try ModuleEnv.init(allocator, ""); - defer module_c_env.deinit(); - - try module_c_env.initCIRFields(allocator, "ModuleC"); - const module_c_cir = &module_c_env; - - // Module C imports from B - _ = try module_c_cir.imports.getOrPut(allocator, &module_c_cir.common.strings, "ModuleB"); - const c_lookup_expr = try module_c_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(1), // Direct index to module B in the array - .target_node_idx = @intCast(@intFromEnum(str_record_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Create array of module environments - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - - try modules.append(module_a_cir); - try modules.append(module_b_cir); - try modules.append(module_c_cir); - - // Type check module B - var checker_b = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker_b.deinit(); - - _ = try checker_b.checkExpr(b_lookup_expr); - _ = try checker_b.checkExpr(str_record_expr_idx); - - // Type check module C - var checker_c = try Check.init(allocator, &module_c_env.types, module_c_cir, modules.items, &module_c_cir.store.regions); - defer checker_c.deinit(); - - _ = try checker_c.checkExpr(c_lookup_expr); - - // Verify the specialized record type in module C - const c_var = @as(Var, @enumFromInt(@intFromEnum(c_lookup_expr))); - const c_resolved = module_c_env.types.resolveVar(c_var); - - try testing.expect(c_resolved.desc.content == .structure); - try testing.expect(c_resolved.desc.content.structure == .record); - - const record = c_resolved.desc.content.structure.record; - const fields = module_c_env.types.getRecordFieldsSlice(record.fields); - try testing.expectEqual(@as(usize, 2), fields.len); - - // Check field names and types - try testing.expectEqualSlices(u8, "value", module_c_env.getIdent(fields.items(.name)[0])); - try testing.expectEqualSlices(u8, "next", module_c_env.getIdent(fields.items(.name)[1])); - - // Check field value is Str - const value_resolved = module_c_env.types.resolveVar(fields.items(.var_)[0]); - try testing.expect(value_resolved.desc.content == .structure); - try testing.expect(value_resolved.desc.content.structure == .str); - - // Check field next is List Str - const next_resolved = module_c_env.types.resolveVar(fields.items(.var_)[1]); - try testing.expect(next_resolved.desc.content == .structure); - try testing.expect(next_resolved.desc.content.structure == .list); - - const list_elem_var = next_resolved.desc.content.structure.list; - const list_elem_resolved = module_c_env.types.resolveVar(list_elem_var); - try testing.expect(list_elem_resolved.desc.content == .structure); - try testing.expect(list_elem_resolved.desc.content.structure == .str); -} - -test "cross-module type checking - complex polymorphic chain with unification" { - const allocator = testing.allocator; - - // Module A exports: compose : (b -> c), (a -> b) -> (a -> c) - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); - - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; - - // Module A exports: compose : (b -> c), (a -> b) -> (a -> c) - const type_var_a = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - const type_var_b = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - const type_var_c = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - - // Create (b -> c) - const b_to_c_content = try module_a_env.types.mkFuncPure(&[_]Var{type_var_b}, type_var_c); - const b_to_c_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), b_to_c_content, base.Region.zero(), Var); - - // Create (a -> b) - const a_to_b_content = try module_a_env.types.mkFuncPure(&[_]Var{type_var_a}, type_var_b); - const a_to_b_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), a_to_b_content, base.Region.zero(), Var); - - // Create (a -> c) - const a_to_c_content = try module_a_env.types.mkFuncPure(&[_]Var{type_var_a}, type_var_c); - const a_to_c_var = try module_a_cir.addTypeSlotAndTypeVar(@enumFromInt(0), a_to_c_content, base.Region.zero(), Var); - - // Create compose : (b -> c), (a -> b) -> (a -> c) - const compose_content = try module_a_env.types.mkFuncPure(&[_]Var{ b_to_c_var, a_to_b_var }, a_to_c_var); - const compose_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, compose_content, base.Region.zero()); - - // Module B imports compose and partially applies with b = Str - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Module B imports from A and makes a new record type - // This is initially a flex var, but the type will be copied from module A - // during type checking - _ = try module_b_cir.imports.getOrPut(allocator, &module_b_cir.common.strings, "ModuleA"); - const b_lookup_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), // Direct index to module A in the array - .target_node_idx = @intCast(@intFromEnum(compose_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Create partially specialized version with b = Str - const str_content = Content{ .structure = .str }; - const b_str_var = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), str_content, base.Region.zero(), Var); - const b_type_var_a = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - const b_type_var_c = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), Content{ .flex_var = null }, base.Region.zero(), Var); - - // Create (Str -> c) - const str_to_c_content = try module_b_env.types.mkFuncPure(&[_]Var{b_str_var}, b_type_var_c); - const str_to_c_var = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), str_to_c_content, base.Region.zero(), Var); - - // Create (a -> Str) - const a_to_str_content = try module_b_env.types.mkFuncPure(&[_]Var{b_type_var_a}, b_str_var); - const a_to_str_var = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), a_to_str_content, base.Region.zero(), Var); - - // Create (a -> c) - const b_a_to_c_content = try module_b_env.types.mkFuncPure(&[_]Var{b_type_var_a}, b_type_var_c); - const b_a_to_c_var = try module_b_cir.addTypeSlotAndTypeVar(@enumFromInt(0), b_a_to_c_content, base.Region.zero(), Var); - - // Create composeStr : (Str -> c), (a -> Str) -> (a -> c) - const compose_str_content = try module_b_env.types.mkFuncPure(&[_]Var{ str_to_c_var, a_to_str_var }, b_a_to_c_var); - - // Create the expression - const compose_str_expr_idx = try module_b_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, compose_str_content, base.Region.zero()); - - // Module C imports the partially specialized version and further specializes c = I32 - var module_c_env = try ModuleEnv.init(allocator, ""); - defer module_c_env.deinit(); - - try module_c_env.initCIRFields(allocator, "ModuleC"); - const module_c_cir = &module_c_env; - - // Module C imports from B and uses the wrapper - // This is initially a flex var, but the type will be copied from module B - // during type checking - _ = try module_c_cir.imports.getOrPut(allocator, &module_c_cir.common.strings, "ModuleB"); - const c_lookup_expr = try module_c_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(1), // Direct index to module B in the array - .target_node_idx = @intCast(@intFromEnum(compose_str_expr_idx)), - .region = base.Region.zero(), - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Create array of module environments - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - - try modules.append(module_a_cir); - try modules.append(module_b_cir); - try modules.append(module_c_cir); - - // Type check module B - var checker_b = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker_b.deinit(); - - _ = try checker_b.checkExpr(b_lookup_expr); - _ = try checker_b.checkExpr(compose_str_expr_idx); - - // Type check module C - var checker_c = try Check.init(allocator, &module_c_env.types, module_c_cir, modules.items, &module_c_cir.store.regions); - defer checker_c.deinit(); - - _ = try checker_c.checkExpr(c_lookup_expr); - - // Important! Now that we've type-checked, we can add vars directly to the - // types store. - - // Now unify the imported function with a specific instantiation - const c_var = @as(Var, @enumFromInt(@intFromEnum(c_lookup_expr))); - - // Create a concrete instance where c = I32 - const i32_content = Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }; - const c_str_var = try module_c_env.types.freshFromContent(str_content); - const c_i32_var = try module_c_env.types.freshFromContent(i32_content); - const c_type_var_a = try module_c_env.types.fresh(); - - // Create (Str -> I32) - const str_to_i32_content = try module_c_env.types.mkFuncPure(&[_]Var{c_str_var}, c_i32_var); - const str_to_i32_var = try module_c_env.types.freshFromContent(str_to_i32_content); - - // Create (a -> Str) - const c_a_to_str_content = try module_c_env.types.mkFuncPure(&[_]Var{c_type_var_a}, c_str_var); - const c_a_to_str_var = try module_c_env.types.freshFromContent(c_a_to_str_content); - - // Create (a -> I32) - const a_to_i32_content = try module_c_env.types.mkFuncPure(&[_]Var{c_type_var_a}, c_i32_var); - const a_to_i32_var = try module_c_env.types.freshFromContent(a_to_i32_content); - - // Create the expected type: (Str -> I32), (a -> Str) -> (a -> I32) - const expected_content = try module_c_env.types.mkFuncPure(&[_]Var{ str_to_i32_var, c_a_to_str_var }, a_to_i32_var); - const expected_var = try module_c_env.types.freshFromContent(expected_content); - - // Unify the imported type with the expected type - const result = try checker_c.unify(c_var, expected_var); - try testing.expect(result.isOk()); - - // Verify the unified type - const c_resolved = module_c_env.types.resolveVar(c_var); - try testing.expect(c_resolved.desc.content == .structure); - try testing.expect(c_resolved.desc.content.structure == .fn_pure); - - const func = c_resolved.desc.content.structure.fn_pure; - const args = module_c_env.types.sliceVars(func.args); - try testing.expectEqual(@as(usize, 2), args.len); - - // First argument should be (Str -> I32) - const first_arg_resolved = module_c_env.types.resolveVar(args[0]); - try testing.expect(first_arg_resolved.desc.content == .structure); - try testing.expect(first_arg_resolved.desc.content.structure == .fn_pure); - - const first_func = first_arg_resolved.desc.content.structure.fn_pure; - const first_func_args = module_c_env.types.sliceVars(first_func.args); - try testing.expectEqual(@as(usize, 1), first_func_args.len); - - const first_func_arg_resolved = module_c_env.types.resolveVar(first_func_args[0]); - try testing.expect(first_func_arg_resolved.desc.content == .structure); - try testing.expect(first_func_arg_resolved.desc.content.structure == .str); - - const first_func_ret_resolved = module_c_env.types.resolveVar(first_func.ret); - try testing.expect(first_func_ret_resolved.desc.content == .structure); - try testing.expect(first_func_ret_resolved.desc.content.structure == .num); - try testing.expect(first_func_ret_resolved.desc.content.structure.num == .int_precision); - try testing.expectEqual(types_mod.Num.Int.Precision.i32, first_func_ret_resolved.desc.content.structure.num.int_precision); - - // Second argument should be (a -> Str) where a is still polymorphic - const second_arg_resolved = module_c_env.types.resolveVar(args[1]); - try testing.expect(second_arg_resolved.desc.content == .structure); - try testing.expect(second_arg_resolved.desc.content.structure == .fn_pure); - - const second_func = second_arg_resolved.desc.content.structure.fn_pure; - const second_func_args = module_c_env.types.sliceVars(second_func.args); - try testing.expectEqual(@as(usize, 1), second_func_args.len); - - const second_func_arg_resolved = module_c_env.types.resolveVar(second_func_args[0]); - try testing.expect(second_func_arg_resolved.desc.content == .flex_var); - - const second_func_ret_resolved = module_c_env.types.resolveVar(second_func.ret); - try testing.expect(second_func_ret_resolved.desc.content == .structure); - try testing.expect(second_func_ret_resolved.desc.content.structure == .str); - - // Return type should be (a -> I32) - const ret_resolved = module_c_env.types.resolveVar(func.ret); - try testing.expect(ret_resolved.desc.content == .structure); - try testing.expect(ret_resolved.desc.content.structure == .fn_pure); - - const ret_func = ret_resolved.desc.content.structure.fn_pure; - const ret_func_args = module_c_env.types.sliceVars(ret_func.args); - try testing.expectEqual(@as(usize, 1), ret_func_args.len); - - const ret_func_arg_resolved = module_c_env.types.resolveVar(ret_func_args[0]); - try testing.expect(ret_func_arg_resolved.desc.content == .flex_var); - - const ret_func_ret_resolved = module_c_env.types.resolveVar(ret_func.ret); - try testing.expect(ret_func_ret_resolved.desc.content == .structure); - try testing.expect(ret_func_ret_resolved.desc.content.structure == .num); - try testing.expect(ret_func_ret_resolved.desc.content.structure.num == .int_precision); - try testing.expectEqual(types_mod.Num.Int.Precision.i32, ret_func_ret_resolved.desc.content.structure.num.int_precision); - - // The 'a' type variable should be consistent across both functions - try testing.expectEqual(second_func_arg_resolved.var_, ret_func_arg_resolved.var_); -} - -test "cross-module type checking - type mismatch with proper error message" { - const allocator = testing.allocator; - - // Module A: Exports a string value - var module_a_env = try ModuleEnv.init(allocator, ""); - defer module_a_env.deinit(); - - try module_a_env.initCIRFields(allocator, "ModuleA"); - const module_a_cir = &module_a_env; - - // Create a string value in module A - const str_expr_idx = try module_a_cir.addExprAndTypeVar(.{ .e_int = .{ - .value = .{ .bytes = [_]u8{0} ** 16, .kind = .i128 }, - } }, .{ .structure = .str }, base.Region.zero()); - - // Module B: Tries to use the string as a number - var module_b_env = try ModuleEnv.init(allocator, ""); - defer module_b_env.deinit(); - - try module_b_env.initCIRFields(allocator, "ModuleB"); - const module_b_cir = &module_b_env; - - // Create an import expression that references module A's string - // This is initially a flex var, but the type will be copied from module A - // during type checking - const import_expr = try module_b_cir.addExprAndTypeVar(.{ - .e_lookup_external = .{ - .module_idx = @enumFromInt(0), - .target_node_idx = @intCast(@intFromEnum(str_expr_idx)), - .region = .{ - .start = .{ .offset = 0 }, - .end = .{ .offset = 20 }, - }, - }, - }, Content{ .flex_var = null }, base.Region.zero()); - - // Set up modules array - var modules = std.ArrayList(*ModuleEnv).init(allocator); - defer modules.deinit(); - try modules.append(module_a_cir); - - // Type check module B - var checker = try Check.init(allocator, &module_b_env.types, module_b_cir, modules.items, &module_b_cir.store.regions); - defer checker.deinit(); - - // Check the import expression - this will copy the type from module A - _ = try checker.checkExpr(import_expr); - - // Important! Now that we've type-checked, we can add vars directly to the - // types store. - - // Now try to unify the import (which has Str type) with I32 - const import_var = @as(types_mod.Var, @enumFromInt(@intFromEnum(import_expr))); - const i32_content = Content{ .structure = .{ .num = .{ .int_precision = .i32 } } }; - const i32_var = try module_b_env.types.freshFromContent(i32_content); - - const result = try checker.unify(import_var, i32_var); - - // The unification should fail - try testing.expect(result.isProblem()); - - // Check that the problem has the cross-module import detail - const problem_idx = result.problem; - const prob = checker.problems.problems.items[@intFromEnum(problem_idx)]; - - try testing.expect(prob == .type_mismatch); - const mismatch = prob.type_mismatch; - - // The detail might be null if the unification happens outside the import handling - // But our code ensures it gets set when the import unification fails - if (mismatch.detail) |detail| { - if (detail == .cross_module_import) { - const cross_module_detail = detail.cross_module_import; - try testing.expectEqual(import_expr, cross_module_detail.import_region); - try testing.expectEqual(@as(CIR.Import.Idx, @enumFromInt(0)), cross_module_detail.module_idx); - } - } + // Identical strings: no replacement + try testing.expect(!displayNameIsBetter("Same", "Same")); + try testing.expect(!displayNameIsBetter("", "")); } diff --git a/src/check/test/custom_num_type_test.zig b/src/check/test/custom_num_type_test.zig new file mode 100644 index 0000000000..8697a6dee0 --- /dev/null +++ b/src/check/test/custom_num_type_test.zig @@ -0,0 +1,137 @@ +//! Tests for custom number types that implement from_numeral + +const std = @import("std"); +const testing = std.testing; +const TestEnv = @import("./TestEnv.zig"); + +test "Custom number type with from_numeral: integer literal unifies" { + const source = + \\ MyNum := [].{ + \\ from_numeral : Numeral -> Try(MyNum, [InvalidNumeral(Str)]) + \\ from_numeral = |_| Err(InvalidNumeral("not supported")) + \\ } + \\ + \\ x : MyNum + \\ x = 42 + ; + + var test_env = try TestEnv.init("MyNum", source); + defer test_env.deinit(); + + // Should type-check successfully - MyNum has from_numeral so it can accept integer literals + try test_env.assertNoErrors(); +} + +test "Custom number type with from_numeral: decimal literal unifies" { + const source = + \\ MyDecimal := [].{ + \\ from_numeral : Numeral -> Try(MyDecimal, [InvalidNumeral(Str)]) + \\ from_numeral = |_| Err(InvalidNumeral("not implemented")) + \\ } + \\ + \\ x : MyDecimal + \\ x = 3.14 + ; + + var test_env = try TestEnv.init("MyDecimal", source); + defer test_env.deinit(); + + // Should type-check successfully - MyDecimal has from_numeral so it can accept decimal literals + try test_env.assertNoErrors(); +} + +test "Custom number type without from_numeral: integer literal does not unify" { + const source = + \\ MyType := [].{ + \\ some_method : MyType -> Bool + \\ some_method = |_| True + \\ } + \\ + \\ x : MyType + \\ x = 42 + ; + + var test_env = try TestEnv.init("MyType", source); + defer test_env.deinit(); + + // Should fail - MyType doesn't have from_numeral + try test_env.assertOneTypeError("MISSING METHOD"); +} + +test "Custom number type with negate: unary minus works" { + const source = + \\ MyNum := [Blah].{ + \\ from_numeral : Numeral -> Try(MyNum, [InvalidNumeral(Str)]) + \\ from_numeral = |_| Err(InvalidNumeral("not implemented")) + \\ + \\ negate : MyNum -> MyNum + \\ negate = |_| Blah + \\ } + \\ + \\ x : MyNum + \\ x = 42 + \\ + \\ y : MyNum + \\ y = -x + ; + + var test_env = try TestEnv.init("MyNum", source); + defer test_env.deinit(); + + // Should type-check successfully - MyNum has negate so unary minus works, + // and Blah is a valid tag in the backing type [Blah] + try test_env.assertNoErrors(); +} + +test "Custom number type without negate: unary minus fails" { + const source = + \\ MyNum := [].{ + \\ from_numeral : Numeral -> Try(MyNum, [InvalidNumeral(Str)]) + \\ from_numeral = |_| Err(InvalidNumeral("not implemented")) + \\ } + \\ + \\ x : MyNum + \\ x = 42 + \\ + \\ y : MyNum + \\ y = -x + ; + + var test_env = try TestEnv.init("MyNum", source); + defer test_env.deinit(); + + // Should fail - MyNum doesn't have negate method + try test_env.assertOneTypeError("MISSING METHOD"); +} + +test "Custom type with negate returning different type" { + // Tests that forward references between sibling types work. + // Positive's negate method returns Negative, which is declared after Positive. + // This requires all type declarations to be introduced into scope before + // processing associated item signatures. + + const source = + \\ Positive := [].{ + \\ from_numeral : Numeral -> Try(Positive, [InvalidNumeral(Str)]) + \\ from_numeral = |_| Err(InvalidNumeral("not implemented")) + \\ + \\ negate : Positive -> Negative + \\ negate = |_| Negative.Value + \\ } + \\ + \\ Negative := [Value] + \\ + \\ x : Positive + \\ x = 5 + \\ + \\ y = -x + ; + + var test_env = try TestEnv.init("Positive", source); + defer test_env.deinit(); + + // Should type-check successfully - Positive can reference Negative in its + // negate method signature even though Negative is declared after Positive. + // The result y should have type Negative. + try test_env.assertNoErrors(); +} diff --git a/src/check/test/generalize_redirect_test.zig b/src/check/test/generalize_redirect_test.zig new file mode 100644 index 0000000000..003463aa6c --- /dev/null +++ b/src/check/test/generalize_redirect_test.zig @@ -0,0 +1,91 @@ +//! Regression test for issue #8656: rank panic when variable redirects to higher-rank variable. +//! +//! The original bug was: when a variable added to the var_pool at rank 1 +//! was later redirected (via setVarRedirect) to a variable at rank 2, +//! generalization would try to add the resolved variable at rank 2 to the +//! tmp_var_pool which only goes up to rank 1, causing a panic. + +const std = @import("std"); +const TestEnv = @import("./TestEnv.zig"); + +test "nested lambda with higher-rank variables does not panic during generalization" { + // This code structure triggered the bug in the original report. + // It involves nested lambdas that create rank-2 variables, pattern matching + // on tuples, and recursive functions. + // + // The key pattern is: + // 1. A function with a nested lambda (ret) that creates rank-2 type variables + // 2. Pattern matching on tuples that exercises the type checker + // 3. Recursive calls that can cause variable redirects across ranks + const source = + \\{ + \\ Maybe(t) : [ + \\ Some(t), + \\ None, + \\ ] + \\ + \\ TokenContents : [ + \\ NewlineToken, + \\ SymbolsToken(Str), + \\ SnakeCaseIdentToken(Str), + \\ EndOfFileToken, + \\ ] + \\ + \\ TokenizerResult : ( + \\ Try(TokenContents, Str), + \\ U64, + \\ U64, + \\ ) + \\ + \\ get_next_token : List(U8), U64 -> TokenizerResult + \\ get_next_token = |file, index| { + \\ match List.get(file, index) { + \\ Ok('\n') => (Ok(NewlineToken), index, index + 1) + \\ Err(_) => (Ok(EndOfFileToken), index, index) + \\ } + \\ } + \\ + \\ tokenize_identifier = |file, index, acc, start_index| { + \\ char = List.get(file, index) + \\ ret = || { + \\ match Str.from_utf8(acc) { + \\ Ok(str) => (Ok(SnakeCaseIdentToken(str)), start_index, index) + \\ Err(_) => (Err("Invalid UTF8"), start_index, index) + \\ } + \\ } + \\ match char { + \\ Ok(c) => { + \\ if ('a' <= c and c <= 'z') or ('A' <= c and c <= 'Z') or (c == '_') { + \\ tokenize_identifier(file, index + 1, List.append(acc, c), start_index) + \\ } else { + \\ ret() + \\ } + \\ } + \\ _ => ret() + \\ } + \\ } + \\ + \\ parse_pattern = |file, tokenizer_result| { + \\ (token, _, index) = tokenizer_result + \\ match token { + \\ Ok(SnakeCaseIdentToken(ident)) => { + \\ match get_next_token(file, index) { + \\ (Ok(SymbolsToken(":")), _, index2) => Ok((ident, Some("type"), index2)) + \\ _ => Ok((ident, None, index)) + \\ } + \\ } + \\ _ => Err("expected pattern") + \\ } + \\ } + \\ + \\ parse_pattern + \\} + ; + var test_env = try TestEnv.initExpr("Test", source); + defer test_env.deinit(); + + // If we get here without panicking, the test passes. + // The bug would cause a panic during type checking with: + // "trying to add var at rank 2, but current rank is 1" + try test_env.assertNoErrors(); +} diff --git a/src/check/test/instantiate_tag_union_test.zig b/src/check/test/instantiate_tag_union_test.zig new file mode 100644 index 0000000000..312c0d4477 --- /dev/null +++ b/src/check/test/instantiate_tag_union_test.zig @@ -0,0 +1,122 @@ +//! Test for instantiating tag unions with tag payloads +//! This test is a regression test for a bug where tag union args were uninitialized. + +const std = @import("std"); +const TestEnv = @import("./TestEnv.zig"); + +test "instantiate polymorphic function with nested recursive tag unions" { + // This tests instantiation of polymorphic functions with nested recursive calls + // that return tag unions with payloads. The code pattern is: + // 1. Multiple mutually-recursive functions return tuples containing Try types + // 2. Pattern matching destructures tuples to extract Try values + // 3. Functions are called multiple times triggering instantiation + // 4. Deep nesting of match expressions + // + // This is a regression test that ensures complex nested tag union patterns + // type-check without panicking. + const source = + \\tokenize_identifier = |file, index, acc, start_index| { + \\ ret = || { + \\ match Str.from_utf8(acc) { + \\ Ok(str) => (Ok(str), start_index, index) + \\ Err(_) => (Err("bad utf8"), start_index, index) + \\ } + \\ } + \\ match List.get(file, index) { + \\ Ok(c) => + \\ if (c >= 97) and (c <= 122) { + \\ tokenize_identifier(file, index + 1, List.append(acc, c), start_index) + \\ } else { + \\ ret() + \\ } + \\ _ => ret() + \\ } + \\} + \\ + \\get_next_token = |file, index| { + \\ match List.get(file, index) { + \\ Ok(c) => + \\ if (c >= 97) and (c <= 122) { + \\ tokenize_identifier(file, index + 1, [c], index) + \\ } else { + \\ (Err("unexpected"), index, index + 1) + \\ } + \\ Err(_) => + \\ (Ok("eof"), index, index) + \\ } + \\} + \\ + \\parse_pattern_match_starting = |file, ident, tokenizer_result| { + \\ (token, _, index) = tokenizer_result + \\ match token { + \\ Ok(s) => Ok((ident, Some(s), token, index)) + \\ _ => Ok((ident, None, token, index)) + \\ } + \\} + \\ + \\parse_pattern_match = |file, tokenizer_result| { + \\ (token, _, index) = tokenizer_result + \\ match token { + \\ Ok(ident) => + \\ parse_pattern_match_starting(file, ident, get_next_token(file, index)) + \\ _ => Err("error") + \\ } + \\} + \\ + \\parse_value = |file, tokenizer_result, possibilities| { + \\ (token, token_pos, index) = tokenizer_result + \\ match token { + \\ Ok(n) => { + \\ match get_next_token(file, index) { + \\ (Ok(_), _, new_index) => Ok((n, new_index)) + \\ _ => Ok((n, index)) + \\ } + \\ } + \\ _ => Err("failed") + \\ } + \\} + \\ + \\parse_block = |file, index, acc| { + \\ match get_next_token(file, index) { + \\ (Ok(n), _, index2) => { + \\ (token, token_pos, index3) = get_next_token(file, index2) + \\ match token { + \\ Ok(_) => { + \\ (args, index4) = parse_function_call_args(file, index3, [])? + \\ parse_block(file, index4, acc) + \\ } + \\ _ => { + \\ (pattern, token4, index4, possibilities) = parse_pattern_match_starting(file, n, (token, token_pos, index3))? + \\ parse_block(file, index4, acc) + \\ } + \\ } + \\ } + \\ got => Err("error") + \\ } + \\} + \\ + \\parse_function_call_args = |file, index, acc| { + \\ (token, token_pos, index2) = get_next_token(file, index) + \\ match token { + \\ Ok(_) => Ok((acc, index2)) + \\ _ => { + \\ (value, index3) = parse_value(file, (token, token_pos, index2), [])? + \\ (token2, token2_pos, index4) = get_next_token(file, index3) + \\ match token2 { + \\ Ok(_) => parse_function_call_args(file, index4, List.append(acc, value)) + \\ _ => Err("error") + \\ } + \\ } + \\ } + \\} + \\ + \\main = { + \\ file = [104, 101, 108, 108, 111] + \\ parse_block(file, 0, []) + \\} + ; + var test_env = try TestEnv.init("Test", source); + defer test_env.deinit(); + + // We just care that it doesn't panic during type checking +} diff --git a/src/check/test/let_polymorphism_integration_test.zig b/src/check/test/let_polymorphism_integration_test.zig index 9b7d9a28ac..483ac48af2 100644 --- a/src/check/test/let_polymorphism_integration_test.zig +++ b/src/check/test/let_polymorphism_integration_test.zig @@ -6,6 +6,7 @@ const base = @import("base"); const parse = @import("parse"); const can = @import("can"); const Check = @import("../Check.zig"); +const TestEnv = @import("./TestEnv.zig"); const Can = can.Can; const ModuleEnv = can.ModuleEnv; @@ -13,33 +14,6 @@ const CanonicalizedExpr = can.Can.CanonicalizedExpr; const testing = std.testing; const test_allocator = testing.allocator; -/// A unified helper to run the full pipeline: parse, canonicalize, and type-check source code. -fn typeCheck(allocator: std.mem.Allocator, source: []const u8) !bool { - // Set up module environment - var module_env = try ModuleEnv.init(allocator, source); - defer module_env.deinit(); - - // Parse - var parse_ast = try parse.parseExpr(&module_env.common, allocator); - defer parse_ast.deinit(allocator); - if (parse_ast.hasErrors()) return false; - - // Canonicalize - var czer = try Can.init(&module_env, &parse_ast, null); - defer czer.deinit(); - - const expr_idx: parse.AST.Expr.Idx = @enumFromInt(parse_ast.root_node_idx); - const canon_expr = try czer.canonicalizeExpr(expr_idx) orelse return false; - - // Type check - var checker = try Check.init(allocator, &module_env.types, &module_env, &.{}, &module_env.store.regions); - defer checker.deinit(); - - _ = try checker.checkExpr(canon_expr.get_idx()); - - return checker.problems.problems.items.len == 0; -} - test "direct polymorphic identity usage" { const source = \\{ @@ -49,7 +23,14 @@ test "direct polymorphic identity usage" { \\ { a, b } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + // The field 'a' has the same type as the dispatcher for from_numeral, so they should share the same name + // Note: 'c' is used because 'a' and 'b' are already identifiers in the code + try typeCheck( + source, + \\{ a: c, b: Str } + \\ where [c.from_numeral : Numeral -> Try(c, [InvalidNumeral(Str)])] + , + ); } test "higher-order function with polymorphic identity" { @@ -62,7 +43,12 @@ test "higher-order function with polymorphic identity" { \\ { a, b } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck( + source, + \\{ a: c, b: Str } + \\ where [c.from_numeral : Numeral -> Try(c, [InvalidNumeral(Str)])] + , + ); } test "let-polymorphism with function composition" { @@ -76,7 +62,7 @@ test "let-polymorphism with function composition" { \\ { result1 } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]"); } test "polymorphic empty list" { @@ -88,50 +74,29 @@ test "polymorphic empty list" { \\ { empty, nums, strs } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck( + source, + \\{ empty: List(_a), nums: List(b), strs: List(Str) } + \\ where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])] + , + ); } test "polymorphic cons function" { - // This test is skipped because these features are missing: - // - Spread operator `..` in list literals [fails at parse stage - syntax not recognized] - // TODO: Enable when spread operator is implemented in the parser - if (true) return error.SkipZigTest; - const source = \\{ - \\ cons = |x, xs| [x, ..xs] + \\ cons = |x, xs| List.concat([x], xs) \\ list1 = cons(1, [2, 3]) \\ list2 = cons("a", ["b", "c"]) \\ { list1, list2 } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); -} - -test "polymorphic map function" { - // This test is skipped because these features are missing: - // - If-then-else expressions [fails at parse stage - syntax not recognized] - // - Recursive function calls [would fail at canonicalize stage - self-references not resolved] - // - List slicing `xs[1..]` [fails at parse stage - range syntax not recognized] - // - Spread operator `[x, ..xs]` [fails at parse stage - syntax not recognized] - // - List equality comparison `xs == []` [may fail at type-check stage] - // Note: List indexing `xs[0]` does parse and canonicalize but may have type issues - // TODO: Enable when conditional expressions, recursion, and list operations are implemented - if (true) return error.SkipZigTest; - - const source = - \\{ - \\ map = |f, xs| - \\ if xs == [] then - \\ [] - \\ else - \\ [f(xs[0]), ..map(f, xs[1..])] - \\ double = |x| x * 2 - \\ nums = map(double, [1, 2, 3]) - \\ { nums } - \\} - ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck( + source, + \\{ list1: List(item), list2: List(Str) } + \\ where [item.from_numeral : Numeral -> Try(item, [InvalidNumeral(Str)])] + , + ); } test "polymorphic record constructor" { @@ -139,12 +104,20 @@ test "polymorphic record constructor" { \\{ \\ make_pair = |x, y| { first: x, second: y } \\ pair1 = make_pair(1, "a") - \\ pair2 = make_pair("hello", 42) - \\ pair3 = make_pair(true, false) + \\ pair2 = make_pair("b", 42) + \\ pair3 = make_pair(True, False) \\ { pair1, pair2, pair3 } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck( + source, + \\{ pair1: { first: a, second: Str }, pair2: { first: Str, second: b }, pair3: { first: [True, .._others], second: [False, .._others2] } } + \\ where [ + \\ a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), + \\ b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), + \\ ] + , + ); } test "polymorphic identity with various numeric types" { @@ -153,11 +126,19 @@ test "polymorphic identity with various numeric types" { \\ id = |x| x \\ int_val = id(42) \\ float_val = id(3.14) - \\ bool_val = id(true) + \\ bool_val = id(True) \\ { int_val, float_val, bool_val } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck( + source, + \\{ bool_val: [True, .._others], float_val: a, int_val: b } + \\ where [ + \\ a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), + \\ b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), + \\ ] + , + ); } test "nested polymorphic data structures" { @@ -170,7 +151,16 @@ test "nested polymorphic data structures" { \\ { box1, box2, nested } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck( + source, + + \\{ box1: { value: a }, box2: { value: Str }, nested: { value: { value: b } } } + \\ where [ + \\ a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), + \\ b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), + \\ ] + , + ); } test "polymorphic function in let binding" { @@ -185,58 +175,34 @@ test "polymorphic function in let binding" { \\ result \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck( + source, + \\{ a: c, b: Str } + \\ where [c.from_numeral : Numeral -> Try(c, [InvalidNumeral(Str)])] + , + ); } test "polymorphic swap function" { - // This test is skipped because these features are missing: - // - Type inference for field access on polymorphic record parameters - // [parses and canonicalizes successfully, fails at type-check stage] - // The syntax `pair.field` works for concrete types but fails when `pair` is - // a polymorphic parameter with fields that have different types across usages. - // The type checker cannot properly infer that `swap` should be polymorphic - // over records with `first` and `second` fields of arbitrary types. - // TODO: Enable when polymorphic record field access type inference is improved - if (true) return error.SkipZigTest; - const source = \\{ \\ swap = |pair| { first: pair.second, second: pair.first } \\ pair1 = { first: 1, second: "a" } - \\ pair2 = { first: true, second: 42 } + \\ pair2 = { first: True, second: 42 } \\ swapped1 = swap(pair1) \\ swapped2 = swap(pair2) \\ { swapped1, swapped2 } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); -} - -test "polymorphic fold function" { - // This test is skipped because these features are missing: - // - If-then-else expressions [fails at parse stage - syntax not recognized] - // - Recursive function calls [would fail at canonicalize stage - self-references not resolved] - // - List equality comparison `xs == []` [may fail at type-check stage] - // - String concatenation operator `++` [fails at parse or canonicalize stage] - // - List slicing `xs[1..]` [fails at parse stage - range syntax not recognized] - // Even if parsing succeeded, the canonicalizer doesn't support recursive - // let-bindings, and the type checker doesn't handle recursive polymorphic functions. - // TODO: Enable when conditional expressions, recursion, and list/string operations are implemented - if (true) return error.SkipZigTest; - - const source = - \\{ - \\ fold = |f, acc, xs| - \\ if xs == [] then - \\ acc - \\ else - \\ fold(f, f(acc, xs[0]), xs[1..]) - \\ sum = fold(|a, b| a + b, 0, [1, 2, 3]) - \\ concat = fold(|a, b| a ++ b, "", ["a", "b", "c"]) - \\ { sum, concat } - \\} - ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck( + source, + \\{ swapped1: { first: Str, second: a }, swapped2: { first: b, second: [True, .._others] } } + \\ where [ + \\ a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), + \\ b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), + \\ ] + , + ); } test "polymorphic option type simulation" { @@ -250,7 +216,10 @@ test "polymorphic option type simulation" { \\ { opt1, opt2, opt3 } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, + \\{ opt1: { tag: Str, value: a }, opt2: { tag: Str, value: Str }, opt3: { tag: Str } } + \\ where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])] + ); } test "polymorphic const function" { @@ -260,39 +229,16 @@ test "polymorphic const function" { \\ always5 = const(5) \\ alwaysHello = const("hello") \\ num = always5(99) - \\ str = alwaysHello(true) + \\ str = alwaysHello(True) \\ { num, str } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); -} - -test "shadowing of polymorphic values" { - // This test is skipped because these features are missing: - // - Type checking for nested block expressions that return values - // [parses and canonicalizes successfully, fails at type-check stage] - // The inner block `{ id = ...; b = ...; b }` should return `b` as its value. - // The type checker fails to properly handle the combination of: - // 1. A nested block that shadows a polymorphic identifier - // 2. The block returning a value (the final `b` expression) - // 3. Continuing to use the original polymorphic `id` after the block - // TODO: Enable when nested block expressions with value returns are fully supported - if (true) return error.SkipZigTest; - - const source = - \\{ - \\ id = |x| x - \\ a = id(1) - \\ inner = { - \\ id = |x| x + 1 // shadows outer id, now monomorphic - \\ b = id(2) - \\ b - \\ } - \\ c = id("test") // uses outer polymorphic id - \\ { a, inner, c } - \\} - ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck( + source, + \\{ num: a, str: Str } + \\ where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])] + , + ); } test "polymorphic pipe function" { @@ -300,11 +246,26 @@ test "polymorphic pipe function" { \\{ \\ pipe = |x, f| f(x) \\ double = |n| n * 2 - \\ length = |s| 5 // simplified string length + \\ length = |_s| 5 \\ num_result = pipe(21, double) \\ str_result = pipe("hello", length) \\ { num_result, str_result } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck( + source, + \\{ num_result: a, str_result: b } + \\ where [ + \\ a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), + \\ b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), + \\ ] + , + ); +} + +/// A unified helper to run the full pipeline: parse, canonicalize, and type-check source code. +fn typeCheck(comptime source_expr: []const u8, expected_type: []const u8) !void { + var test_env = try TestEnv.initExpr("Test", source_expr); + defer test_env.deinit(); + return test_env.assertLastDefType(expected_type); } diff --git a/src/check/test/let_polymorphism_test.zig b/src/check/test/let_polymorphism_test.zig deleted file mode 100644 index f84c8310d3..0000000000 --- a/src/check/test/let_polymorphism_test.zig +++ /dev/null @@ -1,178 +0,0 @@ -//! These tests verify the core type instantiation logic for polymorphic values. - -const std = @import("std"); -const base = @import("base"); -const types = @import("types"); -const can = @import("can"); - -const TypesStore = types.Store; -const ModuleEnv = can.ModuleEnv; -const testing = std.testing; -const test_allocator = testing.allocator; -const Instantiate = types.instantiate.Instantiate; - -// test env // - -const TestEnv = struct { - module_env: *ModuleEnv, - store: *TypesStore, - var_subs: *Instantiate.SeenVars, - rigid_var_subs: *Instantiate.RigidToFlexSubs, - - fn init(allocator: std.mem.Allocator) !TestEnv { - const module_env = try allocator.create(ModuleEnv); - module_env.* = try ModuleEnv.init(allocator, ""); - - const store = try allocator.create(TypesStore); - store.* = try TypesStore.init(allocator); - - const var_subs = try allocator.create(Instantiate.SeenVars); - var_subs.* = Instantiate.SeenVars.init(allocator); - - const rigid_var_subs = try allocator.create(Instantiate.RigidToFlexSubs); - rigid_var_subs.* = try Instantiate.RigidToFlexSubs.init(allocator); - - return .{ - .module_env = module_env, - .store = store, - .var_subs = var_subs, - .rigid_var_subs = rigid_var_subs, - }; - } - - fn deinit(self: *TestEnv, allocator: std.mem.Allocator) void { - self.store.deinit(); - allocator.destroy(self.store); - self.module_env.deinit(); - allocator.destroy(self.module_env); - self.var_subs.deinit(); - allocator.destroy(self.var_subs); - self.rigid_var_subs.deinit(allocator); - allocator.destroy(self.rigid_var_subs); - } - - fn instantiate(self: *TestEnv, var_to_inst: types.Var, rigid_subs: []const struct { ident: []const u8, var_: types.Var }) !types.Var { - self.var_subs.clearRetainingCapacity(); - self.rigid_var_subs.clearFrom(0); - - for (rigid_subs) |sub| { - _ = try self.module_env.insertIdent(base.Ident.for_text(sub.ident)); - try self.rigid_var_subs.append(self.module_env.gpa, .{ .ident = sub.ident, .var_ = sub.var_ }); - } - - var inst = Instantiate.init(self.store, self.module_env.getIdentStore(), self.var_subs); - var instantiate_ctx = Instantiate.Ctx{ - .rigid_var_subs = self.rigid_var_subs, - }; - return inst.instantiateVar(var_to_inst, &instantiate_ctx); - } -}; - -test "let-polymorphism with empty list" { - var env = try TestEnv.init(test_allocator); - defer env.deinit(test_allocator); - - // forall a. List a - const a_ident = try env.module_env.insertIdent(base.Ident.for_text("a")); - const list_elem_var = try env.store.freshFromContent(.{ .rigid_var = a_ident }); - const poly_list_var = try env.store.freshFromContent(.{ .structure = .{ .list = list_elem_var } }); - - try testing.expect(env.store.needsInstantiation(poly_list_var)); - - const int_var = try env.store.freshFromContent(.{ .structure = .{ .num = .{ .num_compact = .{ .int = .i32 } } } }); - const int_list = try env.instantiate(poly_list_var, &.{.{ .ident = "a", .var_ = int_var }}); - - const str_var = try env.store.freshFromContent(.{ .structure = .str }); - const str_list = try env.instantiate(poly_list_var, &.{.{ .ident = "a", .var_ = str_var }}); - - try testing.expect(int_list != str_list); - try testing.expect(int_list != poly_list_var); -} - -test "let-polymorphism with polymorphic function" { - var env = try TestEnv.init(test_allocator); - defer env.deinit(test_allocator); - - // forall a. a -> a - const a_ident = try env.module_env.insertIdent(base.Ident.for_text("a")); - const type_param = try env.store.freshFromContent(.{ .rigid_var = a_ident }); - const func_content = try env.store.mkFuncPure(&.{type_param}, type_param); - const func_var = try env.store.freshFromContent(func_content); - - try testing.expect(env.store.needsInstantiation(func_var)); - - const str_var = try env.store.freshFromContent(.{ .structure = .str }); - const str_func = try env.instantiate(func_var, &.{.{ .ident = "a", .var_ = str_var }}); - - const num_var = try env.store.freshFromContent(.{ .structure = .{ .num = .{ .num_compact = .{ .int = .u32 } } } }); - const num_func = try env.instantiate(func_var, &.{.{ .ident = "a", .var_ = num_var }}); - - try testing.expect(str_func != num_func); -} - -test "let-polymorphism with multiple type parameters" { - var env = try TestEnv.init(test_allocator); - defer env.deinit(test_allocator); - - // forall a b. (a, b) -> (b, a) - const a_ident = try env.module_env.insertIdent(base.Ident.for_text("a")); - const b_ident = try env.module_env.insertIdent(base.Ident.for_text("b")); - const type_a = try env.store.freshFromContent(.{ .rigid_var = a_ident }); - const type_b = try env.store.freshFromContent(.{ .rigid_var = b_ident }); - - const func_content = try env.store.mkFuncPure(&.{ type_a, type_b }, type_b); // Simplified for test - const func_var = try env.store.freshFromContent(func_content); - - const str_var = try env.store.freshFromContent(.{ .structure = .str }); - const int_var = try env.store.freshFromContent(.{ .structure = .{ .num = .{ .num_compact = .{ .int = .i32 } } } }); - - const inst1 = try env.instantiate(func_var, &.{ - .{ .ident = "a", .var_ = str_var }, - .{ .ident = "b", .var_ = int_var }, - }); - - const inst2 = try env.instantiate(func_var, &.{ - .{ .ident = "a", .var_ = int_var }, - .{ .ident = "b", .var_ = str_var }, - }); - - try testing.expect(inst1 != inst2); -} - -test "let-polymorphism preserves sharing within single instantiation" { - var env = try TestEnv.init(test_allocator); - defer env.deinit(test_allocator); - - // forall a. { first: a, second: a } - const a_ident = try env.module_env.insertIdent(base.Ident.for_text("a")); - const type_param = try env.store.freshFromContent(.{ .rigid_var = a_ident }); - - const fields_range = try env.store.record_fields.appendSlice(env.module_env.gpa, &[_]types.RecordField{ - .{ .name = try env.module_env.insertIdent(base.Ident.for_text("first")), .var_ = type_param }, - .{ .name = try env.module_env.insertIdent(base.Ident.for_text("second")), .var_ = type_param }, - }); - const empty_ext = try env.store.freshFromContent(.{ .structure = .empty_record }); - const record_var = try env.store.freshFromContent(.{ .structure = .{ .record = .{ .fields = fields_range, .ext = empty_ext } } }); - - const int_var = try env.store.freshFromContent(.{ .structure = .{ .num = .{ .num_compact = .{ .int = .i32 } } } }); - const instantiated_rec = try env.instantiate(record_var, &.{.{ .ident = "a", .var_ = int_var }}); - - // Verify that both fields now point to the same, new concrete type. - const content = env.store.resolveVar(instantiated_rec).desc.content; - const rec = content.structure.record; - const fields = env.store.record_fields.sliceRange(rec.fields); - - try testing.expectEqual(fields.get(0).var_, fields.get(1).var_); - try testing.expect(env.store.resolveVar(fields.get(0).var_).desc.content.structure.num.num_compact.int == .i32); -} - -test "let-polymorphism prevents over-generalization of concrete types" { - var env = try TestEnv.init(test_allocator); - defer env.deinit(test_allocator); - - const i32_var = try env.store.freshFromContent(.{ .structure = .{ .num = .{ .num_compact = .{ .int = .i32 } } } }); - const list_i32_var = try env.store.freshFromContent(.{ .structure = .{ .list = i32_var } }); - - // This should NOT need instantiation because it's already concrete. - try testing.expect(!env.store.needsInstantiation(list_i32_var)); -} diff --git a/src/check/test/literal_size_test.zig b/src/check/test/literal_size_test.zig deleted file mode 100644 index 03eabd8469..0000000000 --- a/src/check/test/literal_size_test.zig +++ /dev/null @@ -1,592 +0,0 @@ -//! Tests for numeric literal size and type unification logic. - -const std = @import("std"); -const base = @import("base"); -const types = @import("types"); -const can = @import("can"); -const Check = @import("../Check.zig"); - -const TypesStore = types.TypesStore; -const Content = types.Content; -const ModuleEnv = can.ModuleEnv; -const Var = types.Var; -const Num = types.Num; -const problem = @import("../problem.zig"); -const snapshot = @import("../snapshot.zig"); -const occurs = @import("../occurs.zig"); -const FlatType = types.FlatType; -const ProblemStore = problem.Store; -const SnapshotStore = snapshot.Store; -const UnifierScratch = @import("../unify.zig").Scratch; -const OccursScratch = occurs.Scratch; -const unify = @import("../unify.zig").unify; - -test "integer literal 255 fits in U8" { - const gpa = std.testing.allocator; - - var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); - defer module_env.deinit(); - - var problems = try ProblemStore.initCapacity(gpa, 16); - defer problems.deinit(gpa); - - var snapshots = try SnapshotStore.initCapacity(gpa, 16); - defer snapshots.deinit(); - - var scratch = try UnifierScratch.init(gpa); - defer scratch.deinit(); - - var occurs_scratch = try OccursScratch.init(gpa); - defer occurs_scratch.deinit(); - - // Create a literal with value 255 - const literal_var = try module_env.types.freshFromContent(Content{ - .structure = .{ - .num = .{ - .num_poly = .{ - .requirements = Num.IntRequirements{ - .sign_needed = false, - .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"8"), - }, - .var_ = try module_env.types.fresh(), - }, - }, - }, - }); - - // Create U8 type - const u8_var = try module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .int = .u8 } } } }); - - // They should unify successfully - const result = try unify( - &module_env, - &module_env.types, - &problems, - &snapshots, - &scratch, - &occurs_scratch, - literal_var, - u8_var, - ); - - try std.testing.expect(result == .ok); -} - -test "integer literal 256 does not fit in U8" { - const gpa = std.testing.allocator; - - var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); - defer module_env.deinit(); - - var problems = try ProblemStore.initCapacity(gpa, 16); - defer problems.deinit(gpa); - - var snapshots = try SnapshotStore.initCapacity(gpa, 16); - defer snapshots.deinit(); - - var scratch = try UnifierScratch.init(gpa); - defer scratch.deinit(); - - var occurs_scratch = try OccursScratch.init(gpa); - defer occurs_scratch.deinit(); - - // Create a literal with value 256 - const literal_var = try module_env.types.freshFromContent(Content{ - .structure = .{ - .num = .{ - .num_poly = .{ - .requirements = Num.IntRequirements{ - .sign_needed = false, - .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"9_to_15"), - }, - .var_ = try module_env.types.fresh(), - }, - }, - }, - }); - - // Create U8 type - const u8_var = try module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .int = .u8 } } } }); - - // They should NOT unify - type mismatch expected - const result = try unify( - &module_env, - &module_env.types, - &problems, - &snapshots, - &scratch, - &occurs_scratch, - literal_var, - u8_var, - ); - - try std.testing.expect(result == .problem); -} - -test "integer literal -128 fits in I8" { - const gpa = std.testing.allocator; - - var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); - defer module_env.deinit(); - - var problems = try ProblemStore.initCapacity(gpa, 16); - defer problems.deinit(gpa); - - var snapshots = try SnapshotStore.initCapacity(gpa, 16); - defer snapshots.deinit(); - - var scratch = try UnifierScratch.init(gpa); - defer scratch.deinit(); - - var occurs_scratch = try OccursScratch.init(gpa); - defer occurs_scratch.deinit(); - - // Create a literal with value -128 - const literal_var = try module_env.types.freshFromContent(Content{ - .structure = .{ - .num = .{ - .num_poly = .{ - .requirements = Num.IntRequirements{ - .sign_needed = true, - .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), - }, - .var_ = try module_env.types.fresh(), - }, - }, - }, - }); - - // Create I8 type - const i8_var = try module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .int = .i8 } } } }); - - // They should unify successfully - const result = try unify( - &module_env, - &module_env.types, - &problems, - &snapshots, - &scratch, - &occurs_scratch, - literal_var, - i8_var, - ); - - try std.testing.expect(result == .ok); -} - -test "integer literal -129 does not fit in I8" { - const gpa = std.testing.allocator; - - var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); - defer module_env.deinit(); - - var problems = try ProblemStore.initCapacity(gpa, 16); - defer problems.deinit(gpa); - - var snapshots = try SnapshotStore.initCapacity(gpa, 16); - defer snapshots.deinit(); - - var scratch = try UnifierScratch.init(gpa); - defer scratch.deinit(); - - var occurs_scratch = try OccursScratch.init(gpa); - defer occurs_scratch.deinit(); - - // Create a literal with value -129 - const literal_var = try module_env.types.freshFromContent(Content{ - .structure = .{ - .num = .{ - .num_poly = .{ - .requirements = Num.IntRequirements{ - .sign_needed = true, - .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"8"), - }, - .var_ = try module_env.types.fresh(), - }, - }, - }, - }); - - // Create I8 type - const i8_var = try module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .int = .i8 } } } }); - - // They should NOT unify - type mismatch expected - const result = try unify( - &module_env, - &module_env.types, - &problems, - &snapshots, - &scratch, - &occurs_scratch, - literal_var, - i8_var, - ); - - try std.testing.expect(result == .problem); -} - -test "negative literal cannot unify with unsigned type" { - const gpa = std.testing.allocator; - - var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); - defer module_env.deinit(); - - var problems = try ProblemStore.initCapacity(gpa, 16); - defer problems.deinit(gpa); - - var snapshots = try SnapshotStore.initCapacity(gpa, 16); - defer snapshots.deinit(); - - var scratch = try UnifierScratch.init(gpa); - defer scratch.deinit(); - - var occurs_scratch = try OccursScratch.init(gpa); - defer occurs_scratch.deinit(); - - // Create a literal with value -1 - const literal_var = try module_env.types.freshFromContent(Content{ - .structure = .{ - .num = .{ - .num_poly = .{ - .var_ = try module_env.types.fresh(), - .requirements = Num.IntRequirements{ - .sign_needed = true, - .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), - }, - }, - }, - }, - }); - - // Create U8 type - const u8_var = try module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .int = .u8 } } } }); - - // They should NOT unify - type mismatch expected - const result = try unify( - &module_env, - &module_env.types, - &problems, - &snapshots, - &scratch, - &occurs_scratch, - literal_var, - u8_var, - ); - - try std.testing.expect(result == .problem); -} - -test "float literal that fits in F32" { - const gpa = std.testing.allocator; - - var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); - defer module_env.deinit(); - - var problems = try ProblemStore.initCapacity(gpa, 16); - defer problems.deinit(gpa); - - var snapshots = try SnapshotStore.initCapacity(gpa, 16); - defer snapshots.deinit(); - - var scratch = try UnifierScratch.init(gpa); - defer scratch.deinit(); - - var occurs_scratch = try OccursScratch.init(gpa); - defer occurs_scratch.deinit(); - - // Create a literal that fits in F32 - const literal_var = try module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_poly = .{ - .requirements = Num.FracRequirements{ - .fits_in_f32 = true, - .fits_in_dec = true, - }, - .var_ = try module_env.types.fresh(), - } } } }); - - // Create F32 type - const f32_var = try module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .frac = .f32 } } } }); - - // They should unify successfully - const result = try unify( - &module_env, - &module_env.types, - &problems, - &snapshots, - &scratch, - &occurs_scratch, - literal_var, - f32_var, - ); - - try std.testing.expect(result == .ok); -} - -test "float literal that doesn't fit in F32" { - const gpa = std.testing.allocator; - - var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); - defer module_env.deinit(); - - var problems = try ProblemStore.initCapacity(gpa, 16); - defer problems.deinit(gpa); - - var snapshots = try SnapshotStore.initCapacity(gpa, 16); - defer snapshots.deinit(); - - var scratch = try UnifierScratch.init(gpa); - defer scratch.deinit(); - - var occurs_scratch = try OccursScratch.init(gpa); - defer occurs_scratch.deinit(); - - // Create a literal that doesn't fit in F32 (e.g., requires F64 precision) - const literal_var = try module_env.types.freshFromContent(Content{ - .structure = .{ - .num = .{ - .frac_poly = .{ - .requirements = Num.FracRequirements{ - .fits_in_f32 = false, - .fits_in_dec = true, - }, - .var_ = try module_env.types.fresh(), - }, - }, - }, - }); - - // Create F32 type - const f32_var = try module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .frac = .f32 } } } }); - - // They should NOT unify - type mismatch expected - const result = try unify( - &module_env, - &module_env.types, - &problems, - &snapshots, - &scratch, - &occurs_scratch, - literal_var, - f32_var, - ); - - try std.testing.expect(result == .problem); -} - -test "float literal NaN doesn't fit in Dec" { - const gpa = std.testing.allocator; - - var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); - defer module_env.deinit(); - - var problems = try ProblemStore.initCapacity(gpa, 16); - defer problems.deinit(gpa); - - var snapshots = try SnapshotStore.initCapacity(gpa, 16); - defer snapshots.deinit(); - - var scratch = try UnifierScratch.init(gpa); - defer scratch.deinit(); - - var occurs_scratch = try OccursScratch.init(gpa); - defer occurs_scratch.deinit(); - - // Create a literal like NaN that doesn't fit in Dec - const literal_var = try module_env.types.freshFromContent(Content{ - .structure = .{ - .num = .{ - .frac_poly = .{ - .var_ = try module_env.types.fresh(), - .requirements = Num.FracRequirements{ - .fits_in_f32 = true, - .fits_in_dec = false, - }, - }, - }, - }, - }); - - // Create Dec type - const dec_var = try module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .frac = .dec } } } }); - - // They should NOT unify - type mismatch expected - const result = try unify( - &module_env, - &module_env.types, - &problems, - &snapshots, - &scratch, - &occurs_scratch, - literal_var, - dec_var, - ); - - try std.testing.expect(result == .problem); -} - -test "two integer literals with different requirements unify to most restrictive" { - const gpa = std.testing.allocator; - - var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); - defer module_env.deinit(); - - var problems = try ProblemStore.initCapacity(gpa, 16); - defer problems.deinit(gpa); - - var snapshots = try SnapshotStore.initCapacity(gpa, 16); - defer snapshots.deinit(); - - var scratch = try UnifierScratch.init(gpa); - defer scratch.deinit(); - - var occurs_scratch = try OccursScratch.init(gpa); - defer occurs_scratch.deinit(); - - // Create a literal with value 100 (7 bits, no sign) - const literal1_var = try module_env.types.freshFromContent(Content{ - .structure = .{ - .num = .{ - .num_poly = .{ - .var_ = try module_env.types.fresh(), - .requirements = Num.IntRequirements{ - .sign_needed = false, - .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), - }, - }, - }, - }, - }); - - // Create a literal with value 200 (8 bits, no sign) - const literal2_var = try module_env.types.freshFromContent(Content{ - .structure = .{ - .num = .{ - .num_poly = .{ - .var_ = try module_env.types.fresh(), - .requirements = Num.IntRequirements{ - .sign_needed = false, - .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"8"), - }, - }, - }, - }, - }); - - // They should unify successfully - const result = try unify( - &module_env, - &module_env.types, - &problems, - &snapshots, - &scratch, - &occurs_scratch, - literal1_var, - literal2_var, - ); - - try std.testing.expect(result == .ok); - - // Verify that the result has bits_needed = 8 - // After unification, both variables should have the most restrictive requirements - const resolved1 = module_env.types.resolveVar(literal1_var); - switch (resolved1.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .num_poly => |requirements| { - try std.testing.expectEqual(@as(u8, @intFromEnum(Num.Int.BitsNeeded.@"8")), requirements.requirements.bits_needed); - try std.testing.expectEqual(false, requirements.requirements.sign_needed); - }, - .int_poly => |requirements| { - try std.testing.expectEqual(@as(u8, @intFromEnum(Num.Int.BitsNeeded.@"8")), requirements.requirements.bits_needed); - try std.testing.expectEqual(false, requirements.requirements.sign_needed); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - else => return error.UnexpectedContentType, - } -} - -test "positive and negative literals unify with sign requirement" { - const gpa = std.testing.allocator; - - var module_env = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); - defer module_env.deinit(); - - var problems = try ProblemStore.initCapacity(gpa, 16); - defer problems.deinit(gpa); - - var snapshots = try SnapshotStore.initCapacity(gpa, 16); - defer snapshots.deinit(); - - var scratch = try UnifierScratch.init(gpa); - defer scratch.deinit(); - - var occurs_scratch = try OccursScratch.init(gpa); - defer occurs_scratch.deinit(); - - // Create a literal with value 100 (no sign needed) - const literal1_var = try module_env.types.freshFromContent(Content{ - .structure = FlatType{ - .num = Num{ - .num_poly = .{ - .var_ = try module_env.types.fresh(), - .requirements = Num.IntRequirements{ - .sign_needed = false, - .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), - }, - }, - }, - }, - }); - - // Create a literal with value -100 (sign needed) - const literal2_var = try module_env.types.freshFromContent(Content{ - .structure = .{ - .num = .{ - .num_poly = .{ - .var_ = try module_env.types.fresh(), - .requirements = Num.IntRequirements{ - .sign_needed = true, - .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), - }, - }, - }, - }, - }); - - // They should unify successfully - const result = try unify( - &module_env, - &module_env.types, - &problems, - &snapshots, - &scratch, - &occurs_scratch, - literal1_var, - literal2_var, - ); - - try std.testing.expect(result == .ok); - - // Verify that the result has sign_needed = true - // After unification, both variables should have the most restrictive requirements - const resolved1 = module_env.types.resolveVar(literal1_var); - switch (resolved1.desc.content) { - .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .num_poly => |requirements| { - try std.testing.expectEqual(true, requirements.requirements.sign_needed); - try std.testing.expectEqual(@as(u8, @intFromEnum(Num.Int.BitsNeeded.@"7")), requirements.requirements.bits_needed); - }, - .int_poly => |requirements| { - try std.testing.expectEqual(true, requirements.requirements.sign_needed); - try std.testing.expectEqual(@as(u8, @intFromEnum(Num.Int.BitsNeeded.@"7")), requirements.requirements.bits_needed); - }, - else => return error.UnexpectedNumType, - }, - else => return error.UnexpectedStructureType, - }, - else => return error.UnexpectedContentType, - } -} diff --git a/src/check/test/nominal_type_origin_test.zig b/src/check/test/nominal_type_origin_test.zig deleted file mode 100644 index 7d18f66ddb..0000000000 --- a/src/check/test/nominal_type_origin_test.zig +++ /dev/null @@ -1,156 +0,0 @@ -//! Tests for displaying nominal type origins in error messages - -const std = @import("std"); -const base = @import("base"); -const types_mod = @import("types"); - -const Check = @import("../Check.zig"); -const snapshot = @import("../snapshot.zig"); - -const Ident = base.Ident; -const testing = std.testing; -const test_allocator = testing.allocator; - -test "nominal type origin - displays origin in snapshot writer" { - // Create a simple test environment - var idents = try Ident.Store.initCapacity(test_allocator, 16); - defer idents.deinit(test_allocator); - - // Create module name identifiers - const current_module_ident = try idents.insert(test_allocator, Ident.for_text("CurrentModule")); - const other_module_ident = try idents.insert(test_allocator, Ident.for_text("Data.Types")); - const type_name_ident = try idents.insert(test_allocator, Ident.for_text("Person")); - - // Create a snapshot store - var snapshots = try snapshot.Store.initCapacity(test_allocator, 16); - defer snapshots.deinit(); - - // Create a nominal type snapshot with origin from a different module - const nominal_type_backing = snapshot.SnapshotContent{ .structure = .str }; - const nominal_type_backing_idx = try snapshots.contents.append(test_allocator, nominal_type_backing); - const vars_range = try snapshots.content_indexes.appendSlice(test_allocator, &.{nominal_type_backing_idx}); - - const nominal_type = snapshot.SnapshotNominalType{ - .ident = types_mod.TypeIdent{ .ident_idx = type_name_ident }, - .vars = vars_range, - .origin_module = other_module_ident, - }; - - // Test 1: Origin shown when type is from different module - { - var buf = std.ArrayList(u8).init(test_allocator); - defer buf.deinit(); - - var writer = snapshot.SnapshotWriter.init( - buf.writer(), - &snapshots, - &idents, - ); - writer.current_module_name = "CurrentModule"; - - try writer.writeNominalType(nominal_type, nominal_type_backing_idx); - - const result = buf.items; - // Should show "Person (from Data.Types)" - try testing.expect(std.mem.indexOf(u8, result, "Person") != null); - try testing.expect(std.mem.indexOf(u8, result, "(from Data.Types)") != null); - } - - // Test 2: Origin NOT shown when type is from same module - { - var buf = std.ArrayList(u8).init(test_allocator); - defer buf.deinit(); - - // Create a nominal type from the current module - const same_module_nominal = snapshot.SnapshotNominalType{ - .ident = types_mod.TypeIdent{ .ident_idx = type_name_ident }, - .vars = vars_range, - .origin_module = current_module_ident, - }; - - var writer = snapshot.SnapshotWriter.init( - buf.writer(), - &snapshots, - &idents, - ); - writer.current_module_name = "CurrentModule"; - - try writer.writeNominalType(same_module_nominal, nominal_type_backing_idx); - - const result = buf.items; - // Should show just "Person" without origin - try testing.expect(std.mem.indexOf(u8, result, "Person") != null); - try testing.expect(std.mem.indexOf(u8, result, "(from CurrentModule)") == null); - } - - // Test 3: Origin shown with type arguments - { - var buf = std.ArrayList(u8).init(test_allocator); - defer buf.deinit(); - - // Create type arguments - const str_content = snapshot.SnapshotContent{ .structure = .{ .str = {} } }; - const str_idx = try snapshots.contents.append(test_allocator, str_content); - const args_range = try snapshots.content_indexes.appendSlice(test_allocator, &.{ nominal_type_backing_idx, str_idx }); - - // Create a nominal type with args from a different module - const generic_nominal = snapshot.SnapshotNominalType{ - .ident = types_mod.TypeIdent{ .ident_idx = type_name_ident }, - .vars = args_range, - .origin_module = other_module_ident, - }; - - var writer = snapshot.SnapshotWriter.init( - buf.writer(), - &snapshots, - &idents, - ); - writer.current_module_name = "CurrentModule"; - - try writer.writeNominalType(generic_nominal, nominal_type_backing_idx); - - const result = buf.items; - // Should show "Person(Str) (from Data.Types)" - try testing.expect(std.mem.indexOf(u8, result, "Person(Str)") != null); - try testing.expect(std.mem.indexOf(u8, result, "(from Data.Types)") != null); - } -} - -test "nominal type origin - works with no context" { - // Test that the code doesn't crash when context is not provided - var idents = try Ident.Store.initCapacity(test_allocator, 16); - defer idents.deinit(test_allocator); - - const type_name_ident = try idents.insert(test_allocator, Ident.for_text("MyType")); - const module_ident = try idents.insert(test_allocator, Ident.for_text("SomeModule")); - - var snapshots = try snapshot.Store.initCapacity(test_allocator, 16); - defer snapshots.deinit(); - - const nominal_type_backing = snapshot.SnapshotContent{ .structure = .str }; - const nominal_type_backing_idx = try snapshots.contents.append(test_allocator, nominal_type_backing); - const vars_range = try snapshots.content_indexes.appendSlice(test_allocator, &.{nominal_type_backing_idx}); - - const nominal_type = snapshot.SnapshotNominalType{ - .ident = types_mod.TypeIdent{ .ident_idx = type_name_ident }, - .vars = vars_range, - .origin_module = module_ident, - }; - - var buf = std.ArrayList(u8).init(test_allocator); - defer buf.deinit(); - - // Use the basic init without context - var writer = snapshot.SnapshotWriter.init( - buf.writer(), - &snapshots, - &idents, - ); - - try writer.writeNominalType(nominal_type, nominal_type_backing_idx); - - const result = buf.items; - // Should show just "MyType" without origin info - try testing.expect(std.mem.indexOf(u8, result, "MyType") != null); - try testing.expect(std.mem.indexOf(u8, result, "(from") == null); -} diff --git a/src/check/test/num_type_inference_test.zig b/src/check/test/num_type_inference_test.zig new file mode 100644 index 0000000000..b97f2921cc --- /dev/null +++ b/src/check/test/num_type_inference_test.zig @@ -0,0 +1,256 @@ +//! Tests for integer literal type inference +//! +//! This module contains unit tests that verify the correct type inference +//! of integer literals and integer expressions from CIR into the types store. +//! +//! Number literals in the current type system are represented as flex variables +//! with static dispatch constraints for the `from_numeral` method. + +const std = @import("std"); +const testing = std.testing; +const types = @import("types"); +const TestEnv = @import("TestEnv.zig"); +const Content = types.Content; + +test "infers type for small nums" { + const test_cases = [_][]const u8{ + "1", + "-1", + "10", + "-10", + "255", + "-128", + "256", + "-129", + "32767", + "-32768", + "65535", + "-32769", + }; + + inline for (test_cases) |source| { + var test_env = try TestEnv.initExpr("Test", source); + defer test_env.deinit(); + + // Number literals produce flex variables with from_numeral constraints + try test_env.assertLastDefTypeContains("from_numeral"); + } +} + +test "fail to infer num literals outside supported range" { + // Test integer literals that are too big to be represented + const test_cases = [_][]const u8{ + // Negative number slightly lower than i128 min + "-170141183460469231731687303715884105729", + // Number too big for u128 max (340282366920938463463374607431768211455) + "340282366920938463463374607431768211456", + // Way too big + "999999999999999999999999999999999999999999999999999", + // Way, way too big + "999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", + // Way, way too big + "-999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999", + }; + + inline for (test_cases) |source| { + var test_env = try TestEnv.initExpr("Test", source); + defer test_env.deinit(); + + const typ = (try test_env.getLastExprType()).content; + try testing.expect(typ == .err); + } +} + +test "infers type for zero" { + const test_cases = [_][]const u8{ + "0", + "-0", + }; + + inline for (test_cases) |source| { + var test_env = try TestEnv.initExpr("Test", source); + defer test_env.deinit(); + + // Number literals produce flex variables with from_numeral constraints + try test_env.assertLastDefTypeContains("from_numeral"); + } +} + +test "infers type for hex literals" { + const test_cases = [_][]const u8{ + "0x0", + "0x1", + "0xFF", + "0x100", + "0xFFFF", + "-0x1", + "0x1_000", + }; + + inline for (test_cases) |source| { + var test_env = try TestEnv.initExpr("Test", source); + defer test_env.deinit(); + + // Number literals produce flex variables with from_numeral constraints + try test_env.assertLastDefTypeContains("from_numeral"); + } +} + +test "infers type for binary literals" { + const test_cases = [_][]const u8{ + "0b0", + "0b1", + "0b10", + "0b11111111", + "-0b1", + "0b11_11", + }; + + inline for (test_cases) |source| { + var test_env = try TestEnv.initExpr("Test", source); + defer test_env.deinit(); + + // Number literals produce flex variables with from_numeral constraints + try test_env.assertLastDefTypeContains("from_numeral"); + } +} + +test "infers type for octal literals" { + const test_cases = [_][]const u8{ + "0o0", + "0o1", + "0o7", + "0o377", + "-0o1", + "0o1_000", + }; + + inline for (test_cases) |source| { + var test_env = try TestEnv.initExpr("Test", source); + defer test_env.deinit(); + + // Number literals produce flex variables with from_numeral constraints + try test_env.assertLastDefTypeContains("from_numeral"); + } +} + +test "numeric literal in comparison unifies with typed operand" { + // When comparing a typed variable with a numeric literal, + // the literal should unify to match the variable's type. + // `answer == 42` desugars to `answer.is_eq(42)`, which dispatches to I64.is_eq(answer, 42), + // which should unify 42's flex var with I64. + const source = + \\answer : I64 + \\answer = 42 + \\ + \\result = answer == 42 + ; + + var test_env = try TestEnv.init("Test", source); + defer test_env.deinit(); + + // First verify no type errors + try test_env.assertNoErrors(); + + // Verify that `answer` has type I64 + try test_env.assertDefType("answer", "I64"); + + // Verify that `result` has type Bool (the result of ==) + try test_env.assertDefType("result", "Bool"); + + // Now verify that the binop expression's operands both have I64 type + // Find the `result` definition and check the binop's operand types + const ModuleEnv = @import("can").ModuleEnv; + const defs_slice = test_env.module_env.store.sliceDefs(test_env.module_env.all_defs); + var found_result = false; + for (defs_slice) |def_idx| { + const def = test_env.module_env.store.getDef(def_idx); + const ptrn = test_env.module_env.store.getPattern(def.pattern); + if (ptrn == .assign) { + const def_name = test_env.module_env.getIdentStoreConst().getText(ptrn.assign.ident); + if (std.mem.eql(u8, def_name, "result")) { + found_result = true; + // Get the expression - should be a binop + const expr = test_env.module_env.store.getExpr(def.expr); + try testing.expect(expr == .e_binop); + const binop = expr.e_binop; + + // Check LHS type (should be I64) + const lhs_var = ModuleEnv.varFrom(binop.lhs); + try test_env.type_writer.write(lhs_var, .wrap); + const lhs_type = test_env.type_writer.get(); + try testing.expectEqualStrings("I64", lhs_type); + + // Check RHS type (the literal 42 - should also be I64 after unification) + const rhs_var = ModuleEnv.varFrom(binop.rhs); + try test_env.type_writer.write(rhs_var, .wrap); + const rhs_type = test_env.type_writer.get(); + try testing.expectEqualStrings("I64", rhs_type); + + // Verify that the RHS type var is actually resolved to a nominal type, not flex + // This is what the interpreter's translateTypeVar should see + const rhs_resolved = test_env.module_env.types.resolveVar(rhs_var); + // After type checking, the RHS (numeric literal) should be unified to I64, + // which is a nominal type (structure.nominal_type), NOT a flex var + try testing.expect(rhs_resolved.desc.content == .structure); + try testing.expect(rhs_resolved.desc.content.structure == .nominal_type); + break; + } + } + } + try testing.expect(found_result); +} + +test "polymorphic numeric in list used as List.get index unifies to U64 - regression #8666" { + // When a numeric literal is stored in an unannotated list and later used as + // an index to List.get (which takes U64), the type should unify to U64. + // This is a regression test for GitHub issue #8666 where the type remained + // as a flex var, causing the interpreter to default it to Dec layout. + const source = + \\list = [10, 20, 30] + \\index = 0 + \\result = List.get(list, index) + ; + + var test_env = try TestEnv.init("Test", source); + defer test_env.deinit(); + + // First verify no type errors + try test_env.assertNoErrors(); + + // The key assertion: `index` should be U64 after unification with List.get's parameter. + // Find the `index` definition and check its type. + const ModuleEnv = @import("can").ModuleEnv; + const defs_slice = test_env.module_env.store.sliceDefs(test_env.module_env.all_defs); + var found_index = false; + for (defs_slice) |def_idx| { + const def = test_env.module_env.store.getDef(def_idx); + const ptrn = test_env.module_env.store.getPattern(def.pattern); + if (ptrn == .assign) { + const def_name = test_env.module_env.getIdentStoreConst().getText(ptrn.assign.ident); + if (std.mem.eql(u8, def_name, "index")) { + found_index = true; + + // Get the type from the expression (the literal 0) + const expr_var = ModuleEnv.varFrom(def.expr); + try test_env.type_writer.write(expr_var, .wrap); + const expr_type = test_env.type_writer.get(); + + // After unification with List.get's U64 parameter, should be U64 + try testing.expectEqualStrings("U64", expr_type); + + // Also verify the pattern has the same type + const pattern_var = ModuleEnv.varFrom(def.pattern); + try test_env.type_writer.write(pattern_var, .wrap); + const pattern_type = test_env.type_writer.get(); + try testing.expectEqualStrings("U64", pattern_type); + + // Verify the pattern is NOT generalized (numeric literals shouldn't be) + const resolved_pat = test_env.module_env.types.resolveVar(pattern_var); + try testing.expect(resolved_pat.desc.rank != types.Rank.generalized); + break; + } + } + } + try testing.expect(found_index); +} diff --git a/src/check/test/num_type_requirements_test.zig b/src/check/test/num_type_requirements_test.zig new file mode 100644 index 0000000000..695d54a41e --- /dev/null +++ b/src/check/test/num_type_requirements_test.zig @@ -0,0 +1,49 @@ +//! Tests for numeric literal size and type unification logic. + +const std = @import("std"); +const TestEnv = @import("./TestEnv.zig"); + +test "U8: 255 fits" { + const source = + \\{ + \\ x : U8 + \\ x = 50 + \\ + \\ x + 255 + \\} + ; + + var test_env = try TestEnv.initExpr("Test", source); + defer test_env.deinit(); + try test_env.assertLastDefType("U8"); +} + +test "I8: -128 fits" { + const source = + \\{ + \\ x : I8 + \\ x = 1 + \\ + \\ x + -128 + \\} + ; + + var test_env = try TestEnv.initExpr("Test", source); + defer test_env.deinit(); + try test_env.assertLastDefType("I8"); +} + +test "F32: fits" { + const source = + \\{ + \\ x : F32 + \\ x = 1 + \\ + \\ x + 10.1 + \\} + ; + + var test_env = try TestEnv.initExpr("Test", source); + defer test_env.deinit(); + try test_env.assertLastDefType("F32"); +} diff --git a/src/check/test/recursive_alias_test.zig b/src/check/test/recursive_alias_test.zig new file mode 100644 index 0000000000..f74d41c7b8 --- /dev/null +++ b/src/check/test/recursive_alias_test.zig @@ -0,0 +1,84 @@ +//! Tests for recursive alias detection. +//! +//! Type aliases (`:`) cannot be recursive because they are transparent type synonyms. +//! Recursive types must use nominal types (`:=`) instead. + +const std = @import("std"); +const TestEnv = @import("./TestEnv.zig"); + +const testing = std.testing; + +// Direct self-reference tests + +test "recursive alias - direct self-reference without args" { + // Simple recursive alias: A : List(A) + const source = + \\A : List(A) + ; + var test_env = try TestEnv.init("A", source); + defer test_env.deinit(); + try test_env.assertFirstTypeError("RECURSIVE ALIAS"); +} + +test "recursive alias - direct self-reference with args (apply case)" { + // Parameterized recursive alias: Node(a) : { value: a, children: List(Node(a)) } + const source = + \\Node(a) : { value: a, children: List(Node(a)) } + ; + var test_env = try TestEnv.init("Node", source); + defer test_env.deinit(); + try test_env.assertFirstTypeError("RECURSIVE ALIAS"); +} + +// Nominal type recursion is allowed + +test "nominal type - direct self-reference is allowed" { + // Nominal types can be recursive + const source = + \\Node := [Node({ value: Str, children: List(Node) })] + ; + var test_env = try TestEnv.init("Node", source); + defer test_env.deinit(); + // No error - nominal types can be recursive + try test_env.assertNoErrors(); +} + +test "nominal type with args - self-reference is allowed" { + // Parameterized nominal types can be recursive + const source = + \\Tree := [Empty, Node({ value: Str, left: Tree, right: Tree })] + ; + var test_env = try TestEnv.init("Tree", source); + defer test_env.deinit(); + // No error - nominal types can be recursive + try test_env.assertNoErrors(); +} + +// Non-recursive aliases should work + +test "non-recursive alias - simple alias works" { + const source = + \\Point : { x: I64, y: I64 } + ; + var test_env = try TestEnv.init("Point", source); + defer test_env.deinit(); + try test_env.assertNoErrors(); +} + +test "non-recursive alias - parameterized alias works" { + const source = + \\Pair(a, b) : (a, b) + ; + var test_env = try TestEnv.init("Pair", source); + defer test_env.deinit(); + try test_env.assertNoErrors(); +} + +test "non-recursive alias - alias to List works" { + const source = + \\IntList : List(I64) + ; + var test_env = try TestEnv.init("IntList", source); + defer test_env.deinit(); + try test_env.assertNoErrors(); +} diff --git a/src/check/test/static_dispatch_test.zig b/src/check/test/static_dispatch_test.zig deleted file mode 100644 index e3ce7f5ed1..0000000000 --- a/src/check/test/static_dispatch_test.zig +++ /dev/null @@ -1,373 +0,0 @@ -//! Tests for static dispatch on nominal types with method-style syntax - -const std = @import("std"); -const base = @import("base"); -const parse = @import("parse"); -const types_mod = @import("types"); - -const testing = std.testing; -const test_allocator = testing.allocator; - -// NOTE: These tests are currently commented out because they depend on nominal type -// value creation (e.g., `Person { name: "Alice" }` creating a nominal type value). -// The static dispatch implementation is complete and ready to work once nominal -// type value creation is implemented. The type checker can: -// - Detect when a dot access is on a nominal type -// - Find the origin module where the type was defined -// - Look up methods in that module's exports -// - Import and unify the method types correctly - -// test "static dispatch - method call on nominal type in same module" { -// const source = -// \\module [describe] -// \\ -// \\Color : [Red, Green, Blue] -// \\ -// \\describe : Color -> Str -// \\describe = \color -> -// \\ when color is -// \\ Red -> "red" -// \\ Green -> "green" -// \\ Blue -> "blue" -// \\ -// \\main = -// \\ myColor = Red -// \\ myColor.describe() -// ; -// -// var module_env = base.ModuleEnv.init(test_allocator, try test_allocator.dupe(u8, "")); -// defer module_env.deinit(); -// -// // Parse the source -// var parse_ir = parse.parse(&module_env, source); -// defer parse_ir.deinit(test_allocator); -// -// // Create CIR -// var can_ir = CIR.init(&module_env); -// defer can_ir.deinit(); -// can_ir.module_name = "Test"; -// -// // Canonicalize -// var can = try canonicalize.init(&can_ir, &parse_ir, null); -// defer can.deinit(); -// _ = try can.canonicalizeFile(); -// -// // Type check -// var solver = try check_types.init(test_allocator, &module_env.types, &can_ir, &.{}); -// defer solver.deinit(); -// try solver.checkDefs(); -// -// // Verify no type errors -// try testing.expectEqual(@as(usize, 0), solver.problems.problems.len()); -// -// // The type of main should be Str (the result of greet) -// // Find the main definition -// const defs = can_ir.store.sliceDefs(can_ir.all_defs); -// var main_expr_idx: ?CIR.Expr.Idx = null; -// for (defs) |def_idx| { -// const def = can_ir.store.getDef(def_idx); -// const pattern = can_ir.store.getPattern(def.pattern); -// if (pattern == .assign) { -// const ident_idx = pattern.assign.ident; -// const ident_text = can_ir.env.idents.getText(ident_idx); -// -// if (std.mem.eql(u8, ident_text, "main")) { -// main_expr_idx = def.expr; -// break; -// } -// } -// } -// -// try testing.expect(main_expr_idx != null); -// -// // Verify that the type of main is Str -// const main_var = @as(types_mod.Var, @enumFromInt(@intFromEnum(main_expr_idx.?))); -// const resolved_main = module_env.types.resolveVar(main_var); -// -// // The main expression should resolve to Str -// switch (resolved_main.desc.content) { -// .structure => |structure| switch (structure) { -// .str => { -// // Success! The dot access properly resolved to describe which returns Str -// }, -// else => { -// std.debug.print("Expected Str, got: {any}\n", .{structure}); -// try testing.expect(false); -// }, -// }, -// else => { -// std.debug.print("Expected structure, got: {any}\n", .{resolved_main.desc.content}); -// try testing.expect(false); -// }, -// } -// } - -// test "static dispatch - method call on imported nominal type" { -// // Create module environments -// var data_env = base.ModuleEnv.init(test_allocator, try test_allocator.dupe(u8, "")); -// defer data_env.deinit(); -// -// var main_env = base.ModuleEnv.init(test_allocator, try test_allocator.dupe(u8, "")); -// defer main_env.deinit(); -// -// // Create module envs map -// var module_envs = std.StringHashMap(*base.ModuleEnv).init(test_allocator); -// defer module_envs.deinit(); -// try module_envs.put("Data", &data_env); -// -// // Parse Data module -// const data_source = -// \\module [Person, greet] -// \\ -// \\Person := { name: Str, age: U32 } -// \\ -// \\greet : Person -> Str -// \\greet = \person -> "Hello from Data module!" -// ; -// -// var data_parse_ir = parse.parse(&data_env, data_source); -// defer data_parse_ir.deinit(test_allocator); -// -// // Create CIR for Data module -// var data_can_ir = CIR.init(&data_env); -// defer data_can_ir.deinit(); -// data_can_ir.module_name = "Data"; -// -// // Canonicalize Data module -// var data_can = try canonicalize.init(&data_can_ir, &data_parse_ir, &module_envs); -// defer data_can.deinit(); -// _ = try data_can.canonicalizeFile(); -// -// // Type check Data module -// var data_solver = try check_types.init(test_allocator, &data_env.types, &data_can_ir, &.{}); -// defer data_solver.deinit(); -// try data_solver.checkDefs(); -// -// // Parse Main module -// const main_source = -// \\module [] -// \\ -// \\import Data exposing [Person, greet] -// \\ -// \\main = -// \\ bob = Person { name: "Bob", age: 25 } -// \\ bob.greet() -// ; -// -// var main_parse_ir = parse.parse(&main_env, main_source); -// defer main_parse_ir.deinit(test_allocator); -// -// // Create CIR for Main module -// var main_can_ir = CIR.init(&main_env); -// defer main_can_ir.deinit(); -// main_can_ir.module_name = "Main"; -// -// // Canonicalize Main module -// var main_can = try canonicalize.init(&main_can_ir, &main_parse_ir, &module_envs); -// defer main_can.deinit(); -// _ = try main_can.canonicalizeFile(); -// -// // Type check Main module with Data module available -// const other_modules = [_]*CIR{&data_can_ir}; -// var main_solver = try check_types.init(test_allocator, &main_env.types, &main_can_ir, &other_modules); -// defer main_solver.deinit(); -// try main_solver.checkDefs(); -// -// // Verify no type errors -// try testing.expectEqual(@as(usize, 0), main_solver.problems.problems.len()); -// -// // Find the main expression and verify its type -// const defs = main_can_ir.store.sliceDefs(main_can_ir.all_defs); -// var main_expr_idx: ?CIR.Expr.Idx = null; -// for (defs) |def_idx| { -// const def = main_can_ir.store.getDef(def_idx); -// const pattern = main_can_ir.store.getPattern(def.pattern); -// if (pattern == .assign) { -// const ident_idx = pattern.assign.ident; -// const ident_text = main_can_ir.env.idents.getText(ident_idx); -// if (std.mem.eql(u8, ident_text, "main")) { -// main_expr_idx = def.expr; -// break; -// } -// } -// } -// -// try testing.expect(main_expr_idx != null); -// -// // Verify that the type of main is Str -// const main_var = @as(types_mod.Var, @enumFromInt(@intFromEnum(main_expr_idx.?))); -// const resolved_main = main_env.types.resolveVar(main_var); -// -// switch (resolved_main.desc.content) { -// .structure => |structure| switch (structure) { -// .str => { -// // Success! The imported method was properly resolved -// }, -// else => { -// std.debug.print("Expected Str, got: {any}\n", .{structure}); -// try testing.expect(false); -// }, -// }, -// else => { -// std.debug.print("Expected structure, got: {any}\n", .{resolved_main.desc.content}); -// try testing.expect(false); -// }, -// } -// } - -// test "static dispatch - method with multiple arguments" { -// const source = -// \\module [distance] -// \\ -// \\Point := { x: F64, y: F64 } -// \\ -// \\distance : Point, Point -> F64 -// \\distance = \p1, p2 -> -// \\ dx = p1.x - p2.x -// \\ dy = p1.y - p2.y -// \\ Num.sqrt (dx * dx + dy * dy) -// \\ -// \\main = -// \\ origin = Point { x: 0.0, y: 0.0 } -// \\ point = Point { x: 3.0, y: 4.0 } -// \\ origin.distance(point) -// ; -// -// var module_env = base.ModuleEnv.init(test_allocator, try test_allocator.dupe(u8, "")); -// defer module_env.deinit(); -// -// // Parse the source -// var parse_ir = parse.parse(&module_env, source); -// defer parse_ir.deinit(test_allocator); -// -// // Create CIR -// var can_ir = CIR.init(&module_env); -// defer can_ir.deinit(); -// can_ir.module_name = "Test"; -// -// // Canonicalize -// var can = try canonicalize.init(&can_ir, &parse_ir, null); -// defer can.deinit(); -// _ = try can.canonicalizeFile(); -// -// // Type check -// var solver = try check_types.init(test_allocator, &module_env.types, &can_ir, &.{}); -// defer solver.deinit(); -// try solver.checkDefs(); -// -// // Verify no type errors -// try testing.expectEqual(@as(usize, 0), solver.problems.problems.len()); -// -// // Find the main expression -// const defs = can_ir.store.sliceDefs(can_ir.all_defs); -// var main_expr_idx: ?CIR.Expr.Idx = null; -// for (defs) |def_idx| { -// const def = can_ir.store.getDef(def_idx); -// const pattern = can_ir.store.getPattern(def.pattern); -// if (pattern == .assign) { -// const ident_idx = pattern.assign.ident; -// const ident_text = can_ir.env.idents.getText(ident_idx); -// if (std.mem.eql(u8, ident_text, "main")) { -// main_expr_idx = def.expr; -// break; -// } -// } -// } -// -// try testing.expect(main_expr_idx != null); -// -// // Verify that the type of main is F64 -// const main_var = @as(types_mod.Var, @enumFromInt(@intFromEnum(main_expr_idx.?))); -// const resolved_main = module_env.types.resolveVar(main_var); -// -// switch (resolved_main.desc.content) { -// .structure => |structure| switch (structure) { -// .num => |num| { -// switch (num) { -// .frac_precision => |prec| { -// try testing.expect(prec == .f64); -// }, -// else => { -// std.debug.print("Expected frac_precision.f64, got: {any}\n", .{num}); -// try testing.expect(false); -// }, -// } -// }, -// else => { -// std.debug.print("Expected Num.F64, got: {any}\n", .{structure}); -// try testing.expect(false); -// }, -// }, -// else => { -// std.debug.print("Expected structure, got: {any}\n", .{resolved_main.desc.content}); -// try testing.expect(false); -// }, -// } -// } - -// test "static dispatch - error when method not found" { -// const source = -// \\module [] -// \\ -// \\Person := { name: Str, age: U32 } -// \\ -// \\main = -// \\ alice = Person { name: "Alice", age: 30 } -// \\ alice.nonExistentMethod() -// ; -// -// var module_env = base.ModuleEnv.init(test_allocator, try test_allocator.dupe(u8, "")); -// defer module_env.deinit(); -// -// // Parse the source -// var parse_ir = parse.parse(&module_env, source); -// defer parse_ir.deinit(test_allocator); -// -// // Create CIR -// var can_ir = CIR.init(&module_env); -// defer can_ir.deinit(); -// can_ir.module_name = "Test"; -// -// // Canonicalize -// var can = try canonicalize.init(&can_ir, &parse_ir, null); -// defer can.deinit(); -// _ = try can.canonicalizeFile(); -// -// // Type check -// var solver = try check_types.init(test_allocator, &module_env.types, &can_ir, &.{}); -// defer solver.deinit(); -// try solver.checkDefs(); -// -// // We should have problems since the method doesn't exist -// // For now, we just check that the expression was marked as an error -// const defs = can_ir.store.sliceDefs(can_ir.all_defs); -// var main_expr_idx: ?CIR.Expr.Idx = null; -// for (defs) |def_idx| { -// const def = can_ir.store.getDef(def_idx); -// const pattern = can_ir.store.getPattern(def.pattern); -// if (pattern == .assign) { -// const ident_idx = pattern.assign.ident; -// const ident_text = can_ir.env.idents.getText(ident_idx); -// if (std.mem.eql(u8, ident_text, "main")) { -// main_expr_idx = def.expr; -// break; -// } -// } -// } -// -// try testing.expect(main_expr_idx != null); -// -// // The main expression should have an error type -// const main_var = @as(types_mod.Var, @enumFromInt(@intFromEnum(main_expr_idx.?))); -// const resolved_main = module_env.types.resolveVar(main_var); -// -// try testing.expect(resolved_main.desc.content == .err); -// } - -// Placeholder test to ensure the file compiles -test "static dispatch - placeholder for future implementation" { - // This test exists to ensure the test file compiles. - // The actual static dispatch tests are commented out above because they - // depend on nominal type value creation being implemented. - try testing.expect(true); -} diff --git a/src/check/test/type_checking_integration.zig b/src/check/test/type_checking_integration.zig new file mode 100644 index 0000000000..5d1f5c476a --- /dev/null +++ b/src/check/test/type_checking_integration.zig @@ -0,0 +1,2775 @@ +//! Integration tests for let-polymorphism that parse, canonicalize, and type-check +//! actual code to ensure polymorphic values work correctly in practice. + +const std = @import("std"); +const base = @import("base"); +const parse = @import("parse"); +const can = @import("can"); +const types_mod = @import("types"); +const problem_mod = @import("../problem.zig"); +const Check = @import("../Check.zig"); +const TestEnv = @import("./TestEnv.zig"); + +const Can = can.Can; +const ModuleEnv = can.ModuleEnv; +const CanonicalizedExpr = can.Can.CanonicalizedExpr; +const testing = std.testing; +const test_allocator = testing.allocator; + +// primitives - nums // + +test "check type - num - unbound" { + const source = + \\50 + ; + try checkTypesExpr( + source, + .pass, + "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +test "check type - num - int suffix 1" { + const source = + \\10u8 + ; + try checkTypesExpr(source, .pass, "U8"); +} + +test "check type - num - int suffix 2" { + const source = + \\10i128 + ; + try checkTypesExpr(source, .pass, "I128"); +} + +test "check type - num - int big" { + const source = + \\{ + \\ e : U128 + \\ e = 340282366920938463463374607431768211455 + \\ + \\ e + \\} + ; + try checkTypesExpr(source, .pass, "U128"); +} + +test "check type - num - float" { + const source = + \\10.1 + ; + try checkTypesExpr( + source, + .pass, + "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +test "check type - num - float suffix 1" { + const source = + \\10.1f32 + ; + try checkTypesExpr(source, .pass, "F32"); +} + +test "check type - num - float suffix 2" { + const source = + \\10.1f64 + ; + try checkTypesExpr(source, .pass, "F64"); +} + +test "check type - num - float suffix 3" { + const source = + \\10.1dec + ; + try checkTypesExpr(source, .pass, "Dec"); +} + +// primitives - strs // + +test "check type - str" { + const source = + \\"hello" + ; + try checkTypesExpr(source, .pass, "Str"); +} + +test "check type - str annotation mismatch with number" { + const source = + \\x : I64 + \\x = "hello" + ; + try checkTypesModule(source, .fail, "TYPE MISMATCH"); +} + +test "check type - number annotation mismatch with string" { + const source = + \\x : Str + \\x = 42 + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +test "check type - i64 annotation with fractional literal passes type checking" { + // Note: Validation of numeric literals (e.g., fractional to integer) happens + // during comptime evaluation, not type checking. This test verifies that + // type checking passes - the actual validation error is caught by comptime eval. + const source = + \\x : I64 + \\x = 3.14 + ; + try checkTypesModule(source, .{ .pass = .last_def }, "I64"); +} + +test "check type - string plus number should fail" { + // Str + number: when we unify Str with numeric flex, the flex's from_numeral constraint + // gets applied to Str. Since Str doesn't have from_numeral, we get MISSING METHOD. + // The plus dispatch on Str also fails with MISSING METHOD. + const source = + \\x = "hello" + 123 + ; + try checkTypesModule(source, .fail_first, "MISSING METHOD"); +} + +test "check type - string plus string should fail (no plus method)" { + const source = + \\x = "hello" + "world" + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +// binop operand type unification // + +test "check type - binop operands must have same type - I64 plus I32 should fail" { + const source = + \\x = 1i64 + 2i32 + ; + try checkTypesModule(source, .fail, "TYPE MISMATCH"); +} + +test "check type - binop operands must have same type - I64 minus I32 should fail" { + const source = + \\x = 1i64 - 2i32 + ; + try checkTypesModule(source, .fail, "TYPE MISMATCH"); +} + +test "check type - binop operands must have same type - I64 times I32 should fail" { + const source = + \\x = 1i64 * 2i32 + ; + try checkTypesModule(source, .fail, "TYPE MISMATCH"); +} + +test "check type - binop operands must have same type - F64 divide F32 should fail" { + const source = + \\x = 1.0f64 / 2.0f32 + ; + try checkTypesModule(source, .fail, "TYPE MISMATCH"); +} + +test "check type - binop operands same type works - I64 plus I64" { + const source = + \\x = 1i64 + 2i64 + ; + try checkTypesModule(source, .{ .pass = .last_def }, "I64"); +} + +test "check type - binop operands same type works - unbound plus unbound" { + const source = + \\x = 1 + 2 + ; + try checkTypesModule( + source, + .{ .pass = .last_def }, + "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +test "check type - is_eq operands must have same type - I64 eq I32 should fail" { + const source = + \\x = 1i64 == 2i32 + ; + try checkTypesModule(source, .fail, "TYPE MISMATCH"); +} + +test "check type - comparison operands must have same type - I64 lt I32 should fail" { + const source = + \\x = 1i64 < 2i32 + ; + try checkTypesModule(source, .fail, "TYPE MISMATCH"); +} + +// primitives - lists // + +test "check type - list empty" { + const source = + \\[] + ; + try checkTypesExpr(source, .pass, "List(_a)"); +} + +test "check type - list - same elems 1" { + const source = + \\["hello", "world"] + ; + try checkTypesExpr(source, .pass, "List(Str)"); +} + +test "check type - list - same elems 2" { + const source = + \\[100, 200] + ; + try checkTypesExpr( + source, + .pass, + "List(a) where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +test "check type - list - 1st elem more specific coreces 2nd elem" { + const source = + \\[100u64, 200] + ; + try checkTypesExpr(source, .pass, "List(U64)"); +} + +test "check type - list - 2nd elem more specific coreces 1st elem" { + const source = + \\[100, 200u32] + ; + try checkTypesExpr(source, .pass, "List(U32)"); +} + +test "check type - list - diff elems 1" { + const source = + \\["hello", 10] + ; + try checkTypesExpr(source, .fail, "MISSING METHOD"); +} + +// number requirements // + +// Skipped: Literal bounds checking is out of scope for poly removal phase +// See POLY_REMOVAL_PLAN.md +test "check type - num - cannot coerce 500 to u8" { + // const source = + // \\[500, 200u8] + // ; + // try checkTypesExpr(source, .fail, "NUMBER DOES NOT FIT IN TYPE"); +} + +// records // + +test "check type - record" { + const source = + \\{ + \\ hello: "Hello", + \\ world: 10, + \\} + ; + try checkTypesExpr(source, .pass, + \\{ hello: Str, world: a } + \\ where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])] + ); +} + +// anonymous type equality (is_eq) // + +test "check type - record equality - same records are equal" { + const source = + \\{ x: 1, y: 2 } == { x: 1, y: 2 } + ; + try checkTypesExpr(source, .pass, "Bool"); +} + +test "check type - tuple equality - same tuples are equal" { + const source = + \\(1, 2) == (1, 2) + ; + try checkTypesExpr(source, .pass, "Bool"); +} + +test "check type - empty record equality" { + const source = + \\{} == {} + ; + try checkTypesExpr(source, .pass, "Bool"); +} + +test "check type - record with function field - no is_eq" { + // Records containing functions should not have is_eq because functions don't have is_eq + const source = + \\{ x: 1, f: |a| a + 1 } == { x: 1, f: |a| a + 1 } + ; + try checkTypesExpr(source, .fail, "TYPE DOES NOT SUPPORT EQUALITY"); +} + +test "check type - tuple with function element - no is_eq" { + // Tuples containing functions should not have is_eq because functions don't have is_eq + const source = + \\(1, |a| a) == (1, |a| a) + ; + try checkTypesExpr(source, .fail, "TYPE DOES NOT SUPPORT EQUALITY"); +} + +test "check type - nested record equality" { + // Nested records should type-check as Bool + const source = + \\{ a: { x: 1 }, b: 2 } == { a: { x: 1 }, b: 2 } + ; + try checkTypesExpr(source, .pass, "Bool"); +} + +test "check type - nested tuple equality" { + // Nested tuples should type-check as Bool + const source = + \\((1, 2), 3) == ((1, 2), 3) + ; + try checkTypesExpr(source, .pass, "Bool"); +} + +test "check type - nested record with function - no is_eq" { + // Nested records containing functions should not have is_eq + const source = + \\{ a: { f: |x| x } } == { a: { f: |x| x } } + ; + try checkTypesExpr(source, .fail, "TYPE DOES NOT SUPPORT EQUALITY"); +} + +test "check type - tag union equality" { + // Tag unions should type-check for equality + const source = + \\Ok(1) == Ok(1) + ; + try checkTypesExpr(source, .pass, "Bool"); +} + +test "check type - tag union with function payload - no is_eq" { + // Tag unions with function payloads should not have is_eq + const source = + \\Fn(|x| x) == Fn(|x| x) + ; + try checkTypesExpr(source, .fail, "TYPE DOES NOT SUPPORT EQUALITY"); +} + +test "check type - direct lambda equality - no is_eq" { + // Lambdas/functions should not support equality comparison + const source = + \\(|x| x) == (|y| y) + ; + try checkTypesExpr(source, .fail, "TYPE DOES NOT SUPPORT EQUALITY"); +} + +// anonymous type inequality (desugars to is_eq().not()) // + +test "check type - (a == b) desugars to a.is_eq(b) with unified args" { + // `a == b` desugars to `a.is_eq(b)` with additional constraint that a and b have the same type + const src_binop = + \\|a, b| a == b + ; + + // The binop version unifies a and b, so they have the same type variable + const expected_binop: []const u8 = "c, c -> d where [c.is_eq : c, c -> d]"; + try checkTypesExpr(src_binop, .pass, expected_binop); + + // The direct method call version does NOT unify a and b + const src_direct = + \\|a, b| a.is_eq(b) + ; + const expected_direct: []const u8 = "c, d -> e where [c.is_eq : c, d -> e]"; + try checkTypesExpr(src_direct, .pass, expected_direct); +} + +test "check type - (a != b) desugars to a.is_eq(b).not() with unified args" { + // `a != b` desugars to `a.is_eq(b).not()` with additional constraint that a and b have the same type + const src_binop = + \\|a, b| a != b + ; + + // The binop version unifies a and b, so they have the same type variable + const expected_binop: []const u8 = "c, c -> d where [c.is_eq : c, c -> e, e.not : e -> d]"; + try checkTypesExpr(src_binop, .pass, expected_binop); + + // The direct method call version does NOT unify a and b + const src_direct = + \\|a, b| a.is_eq(b).not() + ; + const expected_direct: []const u8 = "c, d -> e where [c.is_eq : c, d -> f, f.not : f -> e]"; + try checkTypesExpr(src_direct, .pass, expected_direct); +} + +test "check type - record inequality - same records" { + // != desugars to is_eq().not(), result type is whatever not returns + const source = + \\{ x: 1, y: 2 } != { x: 1, y: 2 } + ; + // For concrete types, the constraint resolves to Bool since record.is_eq returns Bool and Bool.not returns Bool + try checkTypesExpr(source, .pass, "Bool"); +} + +test "check type - record inequality - diff records" { + const source = + \\{ x: 1, y: 2 } == { x: 1, z: 2 } + ; + try checkTypesExpr(source, .fail, "TYPE MISMATCH"); +} + +test "check type - tuple inequality" { + const source = + \\(1, 2) != (1, 2) + ; + try checkTypesExpr(source, .pass, "Bool"); +} + +test "check type - record with function field - no inequality" { + // Records containing functions should not support != because they don't have is_eq + const source = + \\{ x: 1, f: |a| a + 1 } != { x: 1, f: |a| a + 1 } + ; + try checkTypesExpr(source, .fail, "TYPE DOES NOT SUPPORT EQUALITY"); +} + +test "check type - tuple with function element - no inequality" { + // Tuples containing functions should not support != because they don't have is_eq + const source = + \\(1, |a| a) != (1, |a| a) + ; + try checkTypesExpr(source, .fail, "TYPE DOES NOT SUPPORT EQUALITY"); +} + +test "check type - direct lambda inequality - no is_eq" { + // Lambdas/functions should not support inequality comparison (requires is_eq) + const source = + \\(|x| x) != (|y| y) + ; + try checkTypesExpr(source, .fail, "TYPE DOES NOT SUPPORT EQUALITY"); +} + +test "check type - tag union inequality" { + const source = + \\Ok(1) != Ok(1) + ; + try checkTypesExpr(source, .pass, "Bool"); +} + +test "check type - tag union with function payload - no inequality" { + // Tag unions with function payloads should not support != because they don't have is_eq + const source = + \\Fn(|x| x) != Fn(|x| x) + ; + try checkTypesExpr(source, .fail, "TYPE DOES NOT SUPPORT EQUALITY"); +} + +// tags // + +test "check type - tag" { + const source = + \\MyTag + ; + try checkTypesExpr(source, .pass, "[MyTag, .._others]"); +} + +test "check type - tag - args" { + const source = + \\MyTag("hello", 1) + ; + try checkTypesExpr( + source, + .pass, + \\[MyTag(Str, a), .._others] + \\ where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])] + , + ); +} + +// blocks // + +test "check type - block - return expr" { + const source = + \\{ + \\ "Hello" + \\} + ; + try checkTypesExpr(source, .pass, "Str"); +} + +test "check type - block - implicit empty record" { + const source = + \\{ + \\ _test = "hello" + \\} + ; + try checkTypesExpr(source, .pass, "{}"); +} + +test "check type - block - local value decl" { + const source = + \\{ + \\ test = "hello" + \\ + \\ test + \\} + ; + try checkTypesExpr(source, .pass, "Str"); +} + +// function // + +test "check type - def - value" { + const source = + \\pairU64 = "hello" + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Str"); +} + +test "check type - def - func" { + const source = + \\id = |_| 20 + ; + try checkTypesModule( + source, + .{ .pass = .last_def }, + "_arg -> a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +test "check type - def - id without annotation" { + const source = + \\id = |x| x + ; + try checkTypesModule(source, .{ .pass = .last_def }, "a -> a"); +} + +test "check type - def - id with annotation" { + const source = + \\id : a -> a + \\id = |x| x + ; + try checkTypesModule(source, .{ .pass = .last_def }, "a -> a"); +} + +test "check type - def - func with annotation 1" { + const source = + \\id : x -> Str + \\id = |_| "test" + ; + try checkTypesModule(source, .{ .pass = .last_def }, "x -> Str"); +} + +// TODO: This test is currently failing because annotation parsing doesn't correctly handle +// constraint syntax for flex vars +// This needs to be fixed in the annotation parser, but is separate from the numeric literal work. +test "check type - def - func with annotation 2" { + const source = + \\id : x -> _a + \\id = |_| 15 + ; + // The type annotation says _a is unconstrained, but the implementation returns + // a numeric literal which requires from_numeral method. This is a type error. + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +test "check type - def - nested lambda" { + const source = + \\id = (((|a| |b| |c| a + b + c)(100))(20))(3) + ; + try checkTypesModule( + source, + .{ .pass = .last_def }, + "d where [d.from_numeral : Numeral -> Try(d, [InvalidNumeral(Str)])]", + ); +} + +test "check type - def - forward ref" { + const source = + \\run = id1("howdy") + \\ + \\id1 : x -> x + \\id1 = |x| id2(x) + \\ + \\id2 : x -> x + \\id2 = |x| id3(x) + \\ + \\id3 : x -> x + \\id3 = |x| id4(x) + \\ + \\id4 : x -> x + \\id4 = |x| x + \\ + \\id5 : x -> x + \\id5 = |x| x + ; + try checkTypesModule(source, .{ .pass = .{ .def = "run" } }, "Str"); +} + +test "check type - def - nested lambda with wrong annotation" { + + // Currently the below produces two errors instead of just one. + // NOTE: Num(a) syntax is deprecated - this test may need updating when it's re-enabled + const source = + \\curried_add : Num(a), Num(a), Num(a), Num(a) -> Num(a) + \\curried_add = |a| |b| |c| |d| a + b + c + d + ; + try checkTypesModule(source, .fail, "TYPE MISMATCH"); +} + +// calling functions + +test "check type - def - monomorphic id" { + const source = + \\idStr : Str -> Str + \\idStr = |x| x + \\ + \\test = idStr("hello") + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Str"); +} + +test "check type - def - polymorphic id 1" { + const source = + \\id : x -> x + \\id = |x| x + \\ + \\test = id(5) + ; + try checkTypesModule( + source, + .{ .pass = .last_def }, + "x where [x.from_numeral : Numeral -> Try(x, [InvalidNumeral(Str)])]", + ); +} + +test "check type - def - polymorphic id 2" { + const source = + \\id : x -> x + \\id = |x| x + \\ + \\test = (id(5), id("hello")) + ; + try checkTypesModule( + source, + .{ .pass = .last_def }, + "(x, Str) where [x.from_numeral : Numeral -> Try(x, [InvalidNumeral(Str)])]", + ); +} + +test "check type - def - out of order" { + // Currently errors out in czer + + const source = + \\id_1 : x -> x + \\id_1 = |x| id_2(x) + \\ + \\id_2 : x -> x + \\id_2 = |x| x + \\ + \\test = id_1("Hellor") + ; + try checkTypesModule(source, .{ .pass = .{ .def = "test" } }, "Str"); +} + +test "check type - def - polymorphic higher order 1" { + const source = + \\f = |g, v| g(v) + ; + try checkTypesModule(source, .{ .pass = .last_def }, "(a -> b), a -> b"); +} + +test "check type - top level polymorphic function is generalized" { + const source = + \\id = |x| x + \\ + \\main = { + \\ a = id(42) + \\ _b = id("hello") + \\ a + \\} + ; + try checkTypesModule( + source, + .{ .pass = .last_def }, + "b where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]", + ); +} + +test "check type - let-def polymorphic function is generalized" { + const source = + \\main = { + \\ id = |x| x + \\ a = id(42) + \\ _b = id("hello") + \\ a + \\} + ; + try checkTypesModule( + source, + .{ .pass = .last_def }, + "b where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]", + ); +} + +test "check type - polymorphic function function param should be constrained" { + const source = + \\id = |x| x + \\ + \\use_twice = |f| { + \\ a = f(42) + \\ b = f("hello") + \\ a + \\} + \\result = use_twice(id) + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +// type aliases // + +test "check type - basic alias" { + const source = + \\main! = |_| {} + \\ + \\MyAlias : Str + \\ + \\x : MyAlias + \\x = "hello" + ; + try checkTypesModule(source, .{ .pass = .last_def }, "MyAlias"); +} + +test "check type - alias with arg" { + const source = + \\main! = |_| {} + \\ + \\MyListAlias(a) : List(a) + \\ + \\x : MyListAlias(I64) + \\x = [15] + ; + try checkTypesModule(source, .{ .pass = .last_def }, "MyListAlias(I64)"); +} + +test "check type - alias with mismatch arg" { + const source = + \\MyListAlias(a) : List(a) + \\ + \\x : MyListAlias(Str) + \\x = [15] + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +// nominal types // + +test "check type - basic nominal" { + const source = + \\main! = |_| {} + \\ + \\MyNominal := [MyNominal] + \\ + \\x : MyNominal + \\x = MyNominal.MyNominal + ; + try checkTypesModule(source, .{ .pass = .last_def }, "MyNominal"); +} + +test "check type - nominal with tag arg" { + const source = + \\main! = |_| {} + \\ + \\MyNominal := [MyNominal(Str)] + \\ + \\x : MyNominal + \\x = MyNominal.MyNominal("hello") + ; + try checkTypesModule(source, .{ .pass = .last_def }, "MyNominal"); +} + +test "check type - nominal with type and tag arg" { + const source = + \\main! = |_| {} + \\ + \\MyNominal(a) := [MyNominal(a)] + \\ + \\x : MyNominal(U8) + \\x = MyNominal.MyNominal(10) + ; + try checkTypesModule(source, .{ .pass = .last_def }, "MyNominal(U8)"); +} + +test "check type - nominal with with rigid vars" { + const source = + \\main! = |_| {} + \\ + \\Pair(a) := [Pair(a, a)] + \\ + \\pairU64 : Pair(U64) + \\pairU64 = Pair.Pair(1, 2) + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Pair(U64)"); +} + +test "check type - nominal with with rigid vars mismatch" { + const source = + \\Pair(a) := [Pair(a, a)] + \\ + \\u64val : U64 + \\u64val = 1 + \\ + \\pairU64 : Pair(U64) + \\pairU64 = Pair.Pair(u64val, "Str") + ; + try checkTypesModule(source, .fail, "INVALID NOMINAL TAG"); +} + +test "check type - nominal recursive type" { + const source = + \\main! = |_| {} + \\ + \\ConsList(a) := [Nil, Cons(a, ConsList(a))] + \\ + \\x : ConsList(Str) + \\x = ConsList.Cons("hello", ConsList.Nil) + ; + try checkTypesModule(source, .{ .pass = .last_def }, "ConsList(Str)"); +} + +test "check type - nominal recursive type anno mismatch" { + const source = + \\ConsList(a) := [Nil, Cons(a, ConsList(a))] + \\ + \\x : ConsList(I64) + \\x = ConsList.Cons("hello", ConsList.Nil) + ; + try checkTypesModule(source, .fail, "TYPE MISMATCH"); +} + +test "check type - two nominal types" { + const source = + \\main! = |_| {} + \\ + \\Elem(a) := [Elem(a)] + \\ + \\ConsList(a) := [Nil, Cons(a, ConsList(a))] + \\ + \\x = ConsList.Cons(Elem.Elem("hello"), ConsList.Nil) + ; + try checkTypesModule(source, .{ .pass = .last_def }, "ConsList(Elem(Str))"); +} + +test "check type - nominal recursive type no args" { + const source = + \\main! = |_| {} + \\ + \\StrConsList := [Nil, Cons(Str, StrConsList)] + \\ + \\x : StrConsList + \\x = StrConsList.Cons("hello", StrConsList.Nil) + ; + try checkTypesModule(source, .{ .pass = .last_def }, "StrConsList"); +} + +test "check type - nominal recursive type wrong type" { + const source = + \\StrConsList := [Nil, Cons(Str, StrConsList)] + \\ + \\x : StrConsList + \\x = StrConsList.Cons(10, StrConsList.Nil) + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +test "check type - nominal w/ polymorphic function with bad args" { + const source = + \\Pair(a) := [Pair(a, a)] + \\ + \\mkPairInvalid : a, b -> Pair(a) + \\mkPairInvalid = |x, y| Pair.Pair(x, y) + ; + try checkTypesModule(source, .fail, "INVALID NOMINAL TAG"); +} + +test "check type - nominal w/ polymorphic function" { + const source = + \\main! = |_| {} + \\ + \\Pair(a, b) : (a, b) + \\ + \\swapPair : Pair(a, b) -> Pair(b, a) + \\swapPair = |(x, y)| (y, x) + \\ + \\test = swapPair((1, "test")) + ; + try checkTypesModule( + source, + .{ .pass = .last_def }, + "Pair(Str, a) where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +// bool + +test "check type - bool unqualified" { + const source = + \\x : Bool + \\x = True + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Bool"); +} + +test "check type - bool qualified" { + const source = + \\x = Bool.True + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Bool"); +} + +test "check type - bool lambda" { + const source = + \\x = (|y| !y)(Bool.True) + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Bool"); +} + +// if-else + +test "check type - if else" { + const source = + \\x : Str + \\x = if True "true" else "false" + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Str"); +} + +test "check type - if else - qualified bool" { + const source = + \\x : Str + \\x = if Bool.True "true" else "false" + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Str"); +} + +test "check type - if else - invalid condition 1" { + const source = + \\x : Str + \\x = if 5 "true" else "false" + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +test "check type - if else - invalid condition 2" { + const source = + \\x : Str + \\x = if 10 "true" else "false" + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +test "check type - if else - invalid condition 3" { + const source = + \\x : Str + \\x = if "True" "true" else "false" + ; + try checkTypesModule(source, .fail, "INVALID IF CONDITION"); +} + +test "check type - if else - different branch types 1" { + const source = + \\x = if True "true" else 10 + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +test "check type - if else - different branch types 2" { + const source = + \\x = if True "true" else if False "false" else 10 + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +test "check type - if else - different branch types 3" { + const source = + \\x = if True "true" else if False 10 else "last" + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +// match + +test "check type - match" { + const source = + \\x = + \\ match True { + \\ True => "true" + \\ False => "false" + \\ } + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Str"); +} + +test "check type - match - diff cond types 1" { + const source = + \\x = + \\ match "hello" { + \\ True => "true" + \\ False => "false" + \\ } + ; + try checkTypesModule(source, .fail, "INCOMPATIBLE MATCH PATTERNS"); +} + +test "check type - match - diff branch types" { + const source = + \\x = + \\ match True { + \\ True => "true" + \\ False => 100 + \\ } + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +// unary not + +test "check type - unary not" { + const source = + \\x = !True + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Bool"); +} + +test "check type - unary not mismatch" { + const source = + \\x = !"Hello" + ; + try checkTypesModule(source, .fail, "TYPE MISMATCH"); +} + +// unary not + +test "check type - unary minus" { + const source = + \\x = -10 + ; + try checkTypesModule( + source, + .{ .pass = .last_def }, + "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +test "check type - unary minus mismatch" { + const source = + \\x = "hello" + \\ + \\y = -x + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +// binops + +test "check type - binops math plus" { + const source = + \\x = 10 + 10u32 + ; + try checkTypesModule(source, .{ .pass = .last_def }, "U32"); +} + +test "check type - binops math sub" { + const source = + \\x = 1 - 0.2 + ; + try checkTypesModule( + source, + .{ .pass = .last_def }, + "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +test "check type - binops ord" { + const source = + \\x = 10.0f32 > 15 + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Bool"); +} + +test "check type - binops and" { + const source = + \\x = True and False + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Bool"); +} + +test "check type - binops and mismatch" { + const source = + \\x = "Hello" and False + ; + try checkTypesModule(source, .fail, "INVALID BOOL OPERATION"); +} + +test "check type - binops or" { + const source = + \\x = True or False + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Bool"); +} + +test "check type - binops or mismatch" { + const source = + \\x = "Hello" or False + ; + try checkTypesModule(source, .fail, "INVALID BOOL OPERATION"); +} + +// record access + +test "check type - record - access" { + const source = + \\r = + \\ { + \\ hello: "Hello", + \\ world: 10, + \\ } + \\ + \\x = r.hello + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Str"); +} + +test "check type - record - access func polymorphic" { + const source = + \\x = |r| r.my_field + ; + try checkTypesModule(source, .{ .pass = .last_def }, "{ my_field: a } -> a"); +} + +test "check type - record - access - not a record" { + const source = + \\r = "hello" + \\ + \\x = r.my_field + ; + try checkTypesModule(source, .fail, "TYPE MISMATCH"); +} + +// record update + +test "check type - record - update 1" { + const source = + \\update_data = |container, new_value| { ..container, data: new_value } + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "update_data" } }, + "{ ..a, data: b }, b -> { ..a, data: b }", + ); +} + +test "check type - record - update 2" { + const source = + \\set_data = |container, new_value| { ..container, data: new_value } + \\ + \\updated1 = set_data({ data: 10 }, 100) # Updates field + \\updated2 = set_data({ data: 10, other: "hello" }, 100) # Updates with extra fields + \\updated3 = set_data({ data: "hello" }, "world") # Polymorphic + \\ + \\final = (updated1, updated2, updated3) + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "final" } }, + \\({ data: a }, { data: b, other: Str }, { data: Str }) + \\ where [ + \\ a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), + \\ b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), + \\ ] + , + ); +} + +test "check type - record - update fail" { + const source = + \\set_data = |container, new_value| { ..container, data: new_value } + \\ + \\updated = set_data({ data: "hello" }, 10) + ; + try checkTypesModule( + source, + .fail, + "MISSING METHOD", + ); +} + +// tags // + +test "check type - patterns - wrong type" { + const source = + \\{ + \\ x = True + \\ + \\ match(x) { + \\ "hello" => "world", + \\ } + \\} + ; + try checkTypesExpr(source, .fail, "INCOMPATIBLE MATCH PATTERNS"); +} + +test "check type - patterns tag without payload" { + const source = + \\{ + \\ x = True + \\ + \\ match(x) { + \\ True => "true", + \\ False => "false", + \\ } + \\} + ; + try checkTypesExpr(source, .pass, "Str"); +} + +test "check type - patterns tag with payload" { + const source = + \\{ + \\ x = Ok("ok") + \\ + \\ match(x) { + \\ Ok(val) => val, + \\ Err(_) => "err", + \\ } + \\} + ; + try checkTypesExpr(source, .pass, "Str"); +} + +test "check type - patterns tag with payload mismatch" { + const source = + \\{ + \\ x = Ok("ok") + \\ + \\ match(x) { + \\ Ok(True) => 10 * 10, + \\ Err(_) => 0, + \\ } + \\} + ; + try checkTypesExpr(source, .fail, "INCOMPATIBLE MATCH PATTERNS"); +} + +test "check type - patterns str" { + const source = + \\{ + \\ x = "hello" + \\ + \\ match(x) { + \\ "world" => "true", + \\ _ => "false", + \\ } + \\} + ; + try checkTypesExpr(source, .pass, "Str"); +} + +test "check type - patterns num" { + const source = + \\{ + \\ x = 10 + \\ + \\ match(x) { + \\ 10 => "true", + \\ _ => "false", + \\ } + \\} + ; + try checkTypesExpr(source, .pass, "Str"); +} + +test "check type - patterns int mismatch" { + const source = + \\{ + \\ x = 10u8 + \\ + \\ match(x) { + \\ 10u32 => "true", + \\ _ => "false", + \\ } + \\} + ; + try checkTypesExpr(source, .fail, "INCOMPATIBLE MATCH PATTERNS"); +} + +test "check type - patterns frac 1" { + const source = + \\{ + \\ match(20) { + \\ 10dec as x => x, + \\ _ => 15, + \\ } + \\} + ; + try checkTypesExpr(source, .pass, "Dec"); +} + +test "check type - patterns frac 2" { + const source = + \\{ + \\ match(10) { + \\ 10f32 as x => x, + \\ _ => 15, + \\ } + \\} + ; + try checkTypesExpr(source, .pass, "F32"); +} + +test "check type - patterns frac 3" { + const source = + \\{ + \\ match(50) { + \\ 10 as x => x, + \\ 15f64 as x => x, + \\ _ => 20, + \\ } + \\} + ; + try checkTypesExpr(source, .pass, "F64"); +} + +test "check type - patterns list" { + const source = + \\{ + \\ x = ["a", "b", "c"] + \\ + \\ match(x) { + \\ [.. as b, _a] => b, + \\ [_a, .. as b] => b, + \\ [] => [], + \\ } + \\} + ; + try checkTypesExpr(source, .pass, "List(Str)"); +} + +test "check type - patterns record" { + const source = + \\{ + \\ val = { x: "hello", y: True } + \\ + \\ match(val) { + \\ { y: False } => "False", + \\ { x } => x, + \\ } + \\} + ; + try checkTypesExpr(source, .pass, "Str"); +} + +test "check type - patterns record 2" { + const source = + \\{ + \\ val = { x: "hello", y: True } + \\ + \\ match(val) { + \\ { y: False, x: "world" } => 10 + \\ _ => 20, + \\ } + \\} + ; + try checkTypesExpr( + source, + .pass, + "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +test "check type - patterns record field mismatch" { + const source = + \\{ + \\ val = { x: "hello" } + \\ + \\ match(val) { + \\ { x: False } => 10 + \\ _ => 20 + \\ } + \\} + ; + try checkTypesExpr(source, .fail, "INCOMPATIBLE MATCH PATTERNS"); +} + +// vars + reassignment // + +test "check type - var ressignment" { + const source = + \\main = { + \\ var x = 1 + \\ x = x + 1 + \\ x + \\} + ; + try checkTypesModule( + source, + .{ .pass = .last_def }, + "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +// expect // + +test "check type - expect" { + const source = + \\main = { + \\ x = 1 + \\ expect x == 1 + \\ x + \\} + ; + // Numeric literals with from_numeral constraints are NOT generalized (GitHub #8666). + // This means constraints from `x == 1` (the is_eq constraint) DO propagate back + // to the definition of x, along with the original from_numeral constraint. + try checkTypesModule( + source, + .{ .pass = .last_def }, + \\a + \\ where [ + \\ a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), + \\ a.is_eq : a, a -> Bool, + \\ ] + , + ); +} + +test "check type - expect not bool" { + const source = + \\main = { + \\ x = 1 + \\ expect x + \\ x + \\} + ; + try checkTypesModule(source, .fail, "MISSING METHOD"); +} + +// crash // + +test "check type - crash" { + const source = + \\y : U64 + \\y = { + \\ crash "bug" + \\} + \\ + \\main = { + \\ x = 1 + \\ x + y + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "main" } }, + "U64", + ); +} + +// dbg // + +test "check type - dbg" { + // dbg returns {} (not the value it's debugging), so it can be used + // as a statement/side-effect without affecting the block's return type + const source = + \\y : U64 + \\y = { + \\ dbg 2 + \\ 42 + \\} + \\ + \\main = { + \\ x = 1 + \\ x + y + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "main" } }, + "U64", + ); +} + +// for // + +test "check type - for" { + const source = + \\main = { + \\ var result = 0 + \\ for x in [1, 2, 3] { + \\ result = result + x + \\ } + \\ result + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "main" } }, + "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +test "check type - for mismatch" { + const source = + \\main = { + \\ var result = 0 + \\ for x in ["a", "b", "c"] { + \\ result = result + x + \\ } + \\ result + \\} + ; + try checkTypesModule( + source, + .fail, + "MISSING METHOD", + ); +} + +// static dispatch // + +test "check type - static dispatch - polymorphic - annotation" { + const source = + \\main : a -> Str where [a.to_str : a -> Str] + \\main = |a| a.to_str() + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "main" } }, + "a -> Str where [a.to_str : a -> Str]", + ); +} + +test "check type - static dispatch - polymorphic - no annotation" { + const source = + \\main = |x| x.to_str() + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "main" } }, + "a -> b where [a.to_str : a -> b]", + ); +} + +test "check type - static dispatch - concrete - annotation" { + const source = + \\Test := [Val(Str)].{ + \\ to_str : Test -> Str + \\ to_str = |Test.Val(s)| s + \\} + \\ + \\main : Str + \\main = Test.Val("hello").to_str() + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "main" } }, + "Str", + ); +} + +test "check type - static dispatch - concrete - no annotation" { + const source = + \\Test := [Val(Str)].{ + \\ to_str = |Test.Val(s)| s + \\} + \\ + \\main = Test.Val("hello").to_str() + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "main" } }, + "Str", + ); +} + +test "check type - static dispatch - concrete - wrong method name" { + const source = + \\Test := [Val(Str)].{ + \\ to_str = |Test.Val(s)| s + \\} + \\ + \\main = Test.Val("hello").to_num() + ; + try checkTypesModule( + source, + .fail, + "MISSING METHOD", + ); +} + +test "check type - static dispatch - concrete - args" { + const source = + \\Test := [Val(U8)].{ + \\ add = |Test.Val(a), b| Test.Val(a + b) + \\} + \\ + \\main = Test.Val(1).add(1) + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "main" } }, + "Test", + ); +} + +test "check type - static dispatch - concrete - wrong args" { + const source = + \\Test := [Val(U8)].{ + \\ add = |Test.Val(a), b| Test.Val(a + b) + \\} + \\ + \\main = Test.Val(1).add("hello") + ; + try checkTypesModule( + source, + .fail, + "TYPE MISMATCH", + ); +} + +test "check type - static dispatch - concrete - indirection 1" { + const source = + \\Test := [Val(Str)].{ + \\ to_str = |Test.Val(s)| s + \\ to_str2 = |test| test.to_str() + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "Test.to_str2" } }, + "a -> b where [a.to_str : a -> b]", + ); +} + +test "check type - static dispatch - concrete - indirection 2" { + const source = + \\main! = |_| {} + \\ + \\Test := [Val(Str)].{ + \\ to_str = |Test.Val(s)| s + \\ to_str2 = |test| test.to_str() + \\} + \\ + \\ + \\func = Test.Val("hello").to_str2() + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "func" } }, + "Str", + ); +} + +test "check type - static dispatch - fail if not in type signature" { + const source = + \\main! = |_| {} + \\ + \\func : a -> a + \\func = |a| { + \\ _val = a.method() + \\ a + \\} + ; + try checkTypesModule( + source, + .fail, + "MISSING METHOD", + ); +} + +test "check type - static dispatch - let poly" { + const source = + \\main! = |_| {} + \\ + \\process_container : a -> Str where [a.get_or : a, Str -> Str] + \\process_container = |container| { + \\ result = container.get_or("empty") + \\ result + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "process_container" } }, + "a -> Str where [a.get_or : a, Str -> Str]", + ); +} + +test "check type - static dispatch - let poly 2" { + const source = + \\main! = |_| {} + \\ + \\# Define a Container type with methods + \\Container(a) := [Empty, Value(a)].{ + \\ + \\ # Method to get value or provide default + \\ get_or : Container(a), a -> a + \\ get_or = |container, default| { + \\ match container { + \\ Value(val) => val + \\ Empty => default + \\ } + \\ } + \\} + \\ + \\process_container : a -> Str where [a.get_or : a, Str -> Str] + \\process_container = |container| { + \\ result = container.get_or("empty") + \\ result + \\} + \\ + \\func = { + \\ c = Container.Empty + \\ process_container(c) + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "func" } }, + "Str", + ); +} + +test "check type - static dispatch - polymorphic type" { + const source = + \\main! = |_| {} + \\ + \\Container(a) := [Value(a)].{ + \\ # Method to map over the contained value + \\ map : Container(a), (a -> b) -> Container(b) + \\ map = |Value(val), f| { + \\ Value(f(val)) + \\ } + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "Test.Container.map" } }, + "Container(a), (a -> b) -> Container(b)", + ); +} + +test "check type - static dispatch - polymorphic type 2" { + const source = + \\Container(a) := [Value(a)].{ + \\ # Method to map over the contained value + \\ map : Container(a), (a -> b) -> Container(b) + \\ map = |c, f| { + \\ match c { + \\ Value(val) => Value(f(val)) + \\ } + \\ } + \\} + \\ + \\main! = |_| {} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "Test.Container.map" } }, + "Container(a), (a -> b) -> Container(b)", + ); +} + +test "check type - static dispatch - polymorphic type 3" { + const source = + \\Container(a) := [Empty, Value(a)].{ + \\ # Method to map over the contained value + \\ map : Container(a), (a -> b) -> Container(b) + \\ map = |container, f| { + \\ match container { + \\ Value(val) => Value(f(val)) + \\ Empty => Empty + \\ } + \\ } + \\} + \\ + \\main! = |_| {} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "Test.Container.map" } }, + "Container(a), (a -> b) -> Container(b)", + ); +} + +// comprehensive // + +test "check type - comprehensive - multiple layers of let-polymorphism" { + const source = + \\main! = |_| {} + \\ + \\# First layer: polymorphic identity + \\id : a -> a + \\id = |x| x + \\ + \\# Second layer: uses id polymorphically multiple times + \\apply_twice : (a -> a), a -> a + \\apply_twice = |f, x| { + \\ first = f(x) + \\ second = f(first) + \\ second + \\} + \\ + \\# Third layer: uses apply_twice with different types + \\func = { + \\ num_result = apply_twice(id, 42) + \\ str_result = apply_twice(id, "hello") + \\ bool_result = apply_twice(id, Bool.True) + \\ (num_result, str_result, bool_result) + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "func" } }, + "(a, Str, Bool) where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +test "check type - comprehensive - multiple layers of lambdas" { + const source = + \\main! = |_| {} + \\ + \\# Four layers of nested lambdas + \\curried_add : a, a, a, a -> a where [a.add : a, a -> a] + \\curried_add = |a, b, c, d| a + b + c + d + \\ + \\func = { + \\ step1 = curried_add(1, 2, 3, 4) + \\ step1 + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "func" } }, + \\a + \\ where [ + \\ a.add : a, a -> a, + \\ a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), + \\ ] + , + ); +} + +test "check type - comprehensive - static dispatch with multiple methods" { + const source = + \\main! = |_| {} + \\ + \\# Define a polymorphic container with static dispatch + \\Container(a) := [Empty, Value(a)].{ + \\ # Method with annotation + \\ map : Container(a), (a -> b) -> Container(b) + \\ map = |container, f| { + \\ match container { + \\ Value(val) => Value(f(val)) + \\ Empty => Empty + \\ } + \\ } + \\ + \\ # Method without annotation (inferred) + \\ get_or = |container, default| { + \\ match container { + \\ Container.Value(val) => val + \\ Empty => default + \\ } + \\ } + \\ + \\ # Chained method dispatch + \\ flat_map : Container(a), (a -> Container(b)) -> Container(b) + \\ flat_map = |container, f| { + \\ match container { + \\ Value(val) => f(val) + \\ Empty => Empty + \\ } + \\ } + \\} + \\ + \\func = { + \\ num_container = Container.Value(100) + \\ + \\ chained = num_container + \\ .map(|x| x + 1) + \\ .flat_map(|x| Container.Value(x + 2)) + \\ .get_or(0) + \\ + \\ chained + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "func" } }, + "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]", + ); +} + +test "check type - comprehensive - static dispatch with multiple methods 2" { + const source = + \\main! = |_| {} + \\ + \\Container(a) := [Empty, Value(a)].{ + \\ mapAdd5 = |container| { + \\ container + \\ .mapAdd4() + \\ .mapAdd1() + \\ } + \\ + \\ mapAdd4 = |container| { + \\ container + \\ .mapAdd2() + \\ .mapAdd2() + \\ } + \\ + \\ mapAdd3 = |container| { + \\ container + \\ .mapAdd2() + \\ .mapAdd1() + \\ } + \\ + \\ mapAdd2 = |container| { + \\ container + \\ .mapAdd1() + \\ .mapAdd1() + \\ } + \\ + \\ mapAdd1 = |container| { + \\ container.map(|val| val + 1) + \\ } + \\ + \\ map : Container(a), (a -> b) -> Container(b) + \\ map = |container, f| { + \\ match container { + \\ Value(val) => Value(f(val)) + \\ Empty => Empty + \\ } + \\ } + \\} + \\ + \\func = { + \\ num_container = Container.Value(100) + \\ num_container.mapAdd5() + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "func" } }, + "Container(b) where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]", + ); +} + +// Minimal reproduction test cases for segfault +test "check type - segfault minimal 1 - just annotated plus" { + const source = + \\main! = |_| {} + \\ + \\my_plus : a, a -> a where [a.plus : a, a -> a] + \\my_plus = |x, y| x + y + \\ + \\func = my_plus(1u32, 2u32) + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "func" } }, + "U32", + ); +} + +test "check type - segfault minimal 2 - plus with inferred caller" { + const source = + \\main! = |_| {} + \\ + \\my_plus : a, a -> a where [a.plus : a, a -> a] + \\my_plus = |x, y| x + y + \\ + \\add_two = |a, b| my_plus(a, b) + \\ + \\func = add_two(1u32, 2u32) + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "func" } }, + "U32", + ); +} + +test "check type - segfault minimal 3a - nested direct - SEGFAULTS" { + const source = + \\main! = |_| {} + \\ + \\my_plus : a, a -> a where [a.plus : a, a -> a] + \\my_plus = |x, y| x + y + \\ + \\func = my_plus(my_plus(1u32, 2u32), 3u32) + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "func" } }, + "U32", + ); +} + +test "check type - segfault minimal 3b - nested in lambda - SEGFAULTS" { + const source = + \\main! = |_| {} + \\ + \\my_plus : a, a -> a where [a.plus : a, a -> a] + \\my_plus = |x, y| x + y + \\ + \\add_three = |a, b, c| my_plus(my_plus(a, b), c) + \\ + \\func = add_three(1u32, 2u32, 3u32) + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "func" } }, + "U32", + ); +} + +test "check type - segfault minimal 4 - full original - SEGFAULTS" { + const source = + \\main! = |_| {} + \\ + \\# Annotated function + \\add : a, a -> a where [a.plus : a, a -> a] + \\add = |x, y| x + y + \\ + \\# Inferred function that uses annotated one + \\add_three = |a, b, c| add(add(a, b), c) + \\ + \\# Annotated function using inferred one + \\compute : U32 -> U32 + \\compute = |x| add_three(x, 1u32, 2u32) + \\ + \\func = compute(10u32) + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "func" } }, + "U32", + ); +} + +test "check type - comprehensive: polymorphism + lambdas + dispatch + annotations" { + const source = + \\main! = |_| {} + \\ + \\# Define a polymorphic container with static dispatch + \\Container(a) := [Empty, Value(a)].{ + \\ # Method with annotation + \\ map : Container(a), (a -> b) -> Container(b) + \\ map = |container, f| { + \\ match container { + \\ Value(val) => Value(f(val)) + \\ Empty => Empty + \\ } + \\ } + \\ + \\ # Method without annotation (inferred) + \\ get_or = |container, default| { + \\ match container { + \\ Value(val) => val + \\ Empty => default + \\ } + \\ } + \\ + \\ # Chained method dispatch + \\ flat_map : Container(a), (a -> Container(b)) -> Container(b) + \\ flat_map = |container, f| { + \\ match container { + \\ Value(val) => f(val) + \\ Empty => Empty + \\ } + \\ } + \\} + \\ + \\# First layer: polymorphic helper with annotation + \\compose : (b -> c), (a -> b), a -> c + \\compose = |g, f, x| g(f(x)) + \\ + \\# Second layer: inferred polymorphic function using compose + \\transform_twice = |f, x| { + \\ first = compose(f, f, x) + \\ second = compose(f, f, first) + \\ second + \\} + \\ + \\# Third layer: curried function (multiple lambda layers) + \\make_processor : (a -> b) -> ((b -> c) -> (a -> c)) + \\make_processor = |f1| |f2| |x| { + \\ step1 = f1(x) + \\ step2 = f2(step1) + \\ step2 + \\} + \\ + \\# Fourth layer: polymorphic function using static dispatch + \\process_with_method : a, c -> d where [a.map : a, (b -> c) -> d] + \\process_with_method = |container, value| { + \\ # Multiple nested lambdas with let-polymorphism + \\ id = |x| x + \\ + \\ result = container.map(|_| id(value)) + \\ result + \\} + \\ + \\# Fifth layer: combine everything + \\main = { + \\ # Let-polymorphism layer 1 + \\ # TODO INLINE ANNOS + \\ # id : a -> a + \\ id = |x| x + \\ + \\ # Let-polymorphism layer 2 with nested lambdas + \\ _apply_to_container = |f| |container| |default| { + \\ mapped = container.map(f) + \\ mapped.get_or(default) + \\ } + \\ + \\ # Create containers + \\ num_container = Container.Value(100) + \\ str_container = Container.Value("hello") + \\ _empty_container = Container.Empty + \\ + \\ # Use id polymorphically on different types + \\ id_num = id(42) + \\ id_str = id("world") + \\ id_bool = id(Bool.True) + \\ + \\ # Multiple layers of curried application + \\ add_ten = |x| x + 10 + \\ processor = make_processor(add_ten)(add_ten) + \\ processed = processor(5) + \\ + \\ # Static dispatch with polymorphic methods + \\ num_result = num_container.map(|x| x + 1) + \\ _str_result = str_container.map(|s| s) + \\ + \\ # Chain method calls with static dispatch + \\ chained = num_container + \\ .map(|x| x + 1) + \\ .flat_map(|x| Container.Value(x + 2)) + \\ .get_or(0) + \\ + \\ # Use transform_twice with let-polymorphism + \\ double_fn = |x| x + x + \\ transformed = transform_twice(double_fn, 3) + \\ + \\ # Final result combining all techniques + \\ { + \\ id_results: (id_num, id_str, id_bool), + \\ processed: processed, + \\ chained: chained, + \\ transformed: transformed, + \\ final: num_result.get_or(0), + \\ } + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "main" } }, + \\{ chained: b, final: b, id_results: (e, Str, Bool), processed: c, transformed: a } + \\ where [ + \\ a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), + \\ b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), + \\ b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), + \\ c.from_numeral : Numeral -> Try(c, [InvalidNumeral(Str)]), + \\ e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)]), + \\ ] + , + ); +} + +// scoped type variables + +test "check type - scoped type variables - pass" { + const source = + \\main! = |_| {} + \\ + \\pass : a -> a + \\pass = |x| { + \\ inner : a -> a + \\ inner = |y| y + \\ + \\ inner(x) + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "pass" } }, + "a -> a", + ); +} + +test "check type - scoped type variables - fail" { + const source = + \\main! = |_| {} + \\ + \\fail : a -> a + \\fail = |x| { + \\ g : b -> b + \\ g = |z| z + \\ + \\ result : c + \\ result = g(x) + \\ + \\ result + \\} + ; + try checkTypesModule( + source, + .fail, + "TYPE MISMATCH", + ); +} + +test "check type - scoped type variables - bigger example 1" { + const source = + \\test_scoped : a, b -> a + \\test_scoped = |a, b| { + \\ f : a -> a + \\ f = |z| z + \\ + \\ # No err because we correctly provide `a` as the arg + \\ result : a + \\ result = f(a) + \\ + \\ # Err because we incorrectly provide `b` as the arg + \\ _result2 : b + \\ _result2 = f(b) + \\ + \\ result + \\} + ; + try checkTypesModule( + source, + .fail, + "TYPE MISMATCH", + ); +} + +test "check type - scoped type variables - bigger example 2" { + const source = + \\test : val -> val + \\test = |a| { + \\ b : other_val -> other_val + \\ b = |c| { + \\ d : other_val + \\ d = c + \\ + \\ d + \\ } + \\ + \\ b(a) + \\} + ; + try checkTypesModule( + source, + .{ .pass = .{ .def = "test" } }, + "val -> val", + ); +} + +// Associated items referencing each other + +test "associated item can reference another associated item from same type" { + // First verify Bool basics work + const bool_basics = + \\Test := [].{} + \\ + \\x : Bool + \\x = True + ; + try checkTypesModule(bool_basics, .{ .pass = .{ .def = "x" } }, "Bool"); + + // Now test calling MyBool.my_not from within an associated item + const source = + \\Test := [].{ + \\ MyBool := [MyTrue, MyFalse].{ + \\ my_not : MyBool -> MyBool + \\ my_not = |b| match b { + \\ MyTrue => MyFalse + \\ MyFalse => MyTrue + \\ } + \\ + \\ my_eq : MyBool, MyBool -> MyBool + \\ my_eq = |a, b| match a { + \\ MyTrue => b + \\ MyFalse => MyBool.my_not(b) + \\ } + \\ } + \\} + \\ + \\x = Test.MyBool.my_eq(Test.MyBool.MyTrue, Test.MyBool.MyFalse) + ; + try checkTypesModule(source, .{ .pass = .{ .def = "x" } }, "Test.MyBool"); +} + +test "Bool.not works as builtin associated item" { + const source = + \\Test := [].{} + \\ + \\x = Bool.not(True) + ; + try checkTypesModule(source, .{ .pass = .{ .def = "x" } }, "Bool"); +} + +test "Str.is_empty works as low-level builtin associated item" { + const source = + \\Test := [].{} + \\ + \\x = Str.is_empty("") + ; + try checkTypesModule(source, .{ .pass = .{ .def = "x" } }, "Bool"); +} + +test "List.fold works as builtin associated item" { + const source = + \\Test := [].{} + \\ + \\x = List.fold([1, 2, 3], 0, |acc, item| acc + item) + ; + try checkTypesModule(source, .{ .pass = .{ .def = "x" } }, "item where [item.from_numeral : Numeral -> Try(item, [InvalidNumeral(Str)])]"); +} + +test "associated item: type annotation followed by body should not create duplicate definition" { + const source = + \\Test := [].{ + \\ apply : (a -> b), a -> b + \\ apply = |fn, x| fn(x) + \\} + \\ + \\result = Test.apply(|n| n, 42) + ; + + var test_env = try TestEnv.init("Test", source); + defer test_env.deinit(); + + // Should have NO errors - the type annotation should be associated with the body + const can_diagnostics = try test_env.module_env.getDiagnostics(); + defer test_env.gpa.free(can_diagnostics); + const type_problems = test_env.checker.problems.problems.items; + + try testing.expectEqual(@as(usize, 0), can_diagnostics.len); + try testing.expectEqual(@as(usize, 0), type_problems.len); + + // Verify the types + try test_env.assertDefType("Test.apply", "(a -> b), a -> b"); + try test_env.assertDefType("result", "b where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]"); +} + +// TODO: Move this test to can +test "top-level: type annotation followed by body should not create duplicate definition - REGRESSION TEST" { + // This reproduces the bug seen in test/snapshots/pass/underscore_in_regular_annotations.md + // and test/snapshots/type_function_simple.md where a type annotation followed by its body + // creates TWO defs: + // 1. A def with e-anno-only for the annotation + // 2. A def with the actual lambda body + // This causes a DUPLICATE DEFINITION error + // + // NOTE: Using EXACT code from the snapshot that shows the bug! + const source = + \\app [main!] { pf: platform "platform.roc" } + \\ + \\apply : (_a -> _b) -> _a -> _b + \\apply = |fn, x| fn(x) + \\ + \\main! = |_| {} + ; + + var test_env = try TestEnv.init("Test", source); + defer test_env.deinit(); + + // Check for canonicalization problems - should be specifically DUPLICATE DEFINITION + const can_diagnostics = try test_env.module_env.getDiagnostics(); + defer test_env.gpa.free(can_diagnostics); + + var duplicate_def_found = false; + for (can_diagnostics) |diagnostic| { + var report = try test_env.module_env.diagnosticToReport(diagnostic, test_env.gpa, test_env.module_env.module_name); + defer report.deinit(); + + if (std.mem.indexOf(u8, report.title, "DUPLICATE DEFINITION") != null) { + duplicate_def_found = true; + break; + } + } + + // The bug causes a DUPLICATE DEFINITION error - this test should FAIL when bug is present + if (duplicate_def_found) { + return error.TestUnexpectedResult; + } +} + +// equirecursive static dispatch // + +test "check type - equirecursive static dispatch" { + // Tests that method dispatch works with numeric literals + // The expression (|x| x.plus(5))(7) should type-check successfully + const source = "(|x| x.plus(5))(7)"; + + try checkTypesExpr( + source, + .pass, + "_a", + ); +} + +test "check type - equirecursive static dispatch with type annotation" { + // This tests the exact pattern from the example (|x| x.plus(b))(a) + // but with explicit type annotations. + // This demonstrates that the RecursionVar infrastructure works correctly + // with the same constraint structure as the motivating example. + const source = + \\fn : a, b -> ret where [ + \\ a.plus : a, b -> ret, + \\ a.from_int_digits : List(U8) -> Try(a, [OutOfRange]), + \\ b.from_int_digits : List(U8) -> Try(b, [OutOfRange]) + \\] + \\fn = |a, b| (|x| x.plus(b))(a) + ; + + // The annotated type should match the inferred type + try checkTypesModule( + source, + .{ .pass = .{ .def = "fn" } }, + \\a, b -> ret + \\ where [ + \\ a.from_int_digits : List(U8) -> Try(a, [OutOfRange]), + \\ a.plus : a, b -> ret, + \\ b.from_int_digits : List(U8) -> Try(b, [OutOfRange]), + \\ ] + , + ); +} + +test "check type - static dispatch method type mismatch - REGRESSION TEST" { + // This test verifies that when a method is called with mismatched types, + // we get a TYPE MISMATCH error. This is a regression test for the diagnostic + // output when static dispatch method arguments don't match. + // + // The scenario: a function requires is_eq on type `a`, but we call it + // with two different types (number and string), causing a type mismatch. + const source = + \\fn : a, a -> Bool where [a.is_eq : a, a -> Bool] + \\fn = |x, y| x.is_eq(y) + \\ + \\result = fn(1u64, 2u64) == fn(3u64, 4u64) + ; + + // This should pass - both calls use the same types + try checkTypesModule(source, .{ .pass = .last_def }, "Bool"); +} + +// helpers - module // + +const ModuleExpectation = union(enum) { + pass: DefExpectation, + fail, + fail_first, // Allows multiple errors, checks first error title +}; + +const DefExpectation = union(enum) { + last_def, + def: []const u8, +}; + +/// A unified helper to run the full pipeline: parse, canonicalize, and type-check source code. +/// +/// Behavior depends on the expectation: +/// Pass: Asserts whole module type checks, and assert the specified def matches the expected type string +/// Fail: Asserts that there is exactly 1 type error in the module and it's title matches the expected string +fn checkTypesModule( + comptime source_expr: []const u8, + comptime expectation: ModuleExpectation, + comptime expected: []const u8, +) !void { + var test_env = try TestEnv.init("Test", source_expr); + defer test_env.deinit(); + + switch (expectation) { + .pass => |def_expectation| { + switch (def_expectation) { + .last_def => { + return test_env.assertLastDefType(expected); + }, + .def => |def_name| { + return test_env.assertDefType(def_name, expected); + }, + } + }, + .fail => { + return test_env.assertOneTypeError(expected); + }, + .fail_first => { + return test_env.assertFirstTypeError(expected); + }, + } + + return test_env.assertLastDefType(expected); +} + +// helpers - expr // + +const ExprExpectation = union(enum) { + pass, + fail, +}; + +/// A unified helper to run the full pipeline: parse, canonicalize, and type-check source code. +/// +/// Behavior depends on the expectation: +/// Pass: Asserts expr type checks, and asserts that the expr's type match the expected type string +/// Fail: Asserts that there is exactly 1 type error and it's title matches the expected string +fn checkTypesExpr( + comptime source_expr: []const u8, + comptime expectation: ExprExpectation, + comptime expected: []const u8, +) !void { + var test_env = try TestEnv.initExpr("Test", source_expr); + defer test_env.deinit(); + + switch (expectation) { + .pass => { + return test_env.assertLastDefType(expected); + }, + .fail => { + return test_env.assertOneTypeError(expected); + }, + } + + return test_env.assertLastDefType(expected); +} + +// effectful function type annotation parsing // + +test "check type - effectful zero-arg function annotation" { + // This test verifies that () => {} is parsed as a zero-arg effectful function, + // NOT as a function taking a unit tuple argument. + // The bug was that () => {} was being parsed as (()) => {} - a function taking + // one empty-tuple argument instead of zero arguments. + const source = + \\foo : (() => {}) + \\foo = || {} + ; + // Expected: zero-arg effectful function returning empty record + // If the parser bug exists, this would fail with TYPE MISMATCH because: + // - annotation parses as: (()) => {} (one empty-tuple arg) + // - lambda infers as: ({}) -> {} (zero args, pure) + try checkTypesModule(source, .{ .pass = .last_def }, "({}) => { }"); +} + +test "check type - pure zero-arg function annotation" { + // This test verifies that () -> {} is parsed as a zero-arg pure function, + // NOT as a function taking a unit tuple argument. + const source = + \\foo : (() -> {}) + \\foo = || {} + ; + // Expected: zero-arg pure function returning empty record + try checkTypesModule(source, .{ .pass = .last_def }, "({}) -> { }"); +} + +test "qualified imports don't produce MODULE NOT FOUND during canonicalization" { + // Qualified imports (e.g., "json.Json") are cross-package imports that are + // resolved by the workspace resolver, not during canonicalization. + // They should NOT produce MODULE NOT FOUND errors during canonicalization. + // + // Source from test/snapshots/can_import_comprehensive.md + const source = + \\import json.Json + \\import http.Client as Http exposing [get, post] + \\import utils.String as Str + \\ + \\main = { + \\ client = Http.get + \\ parser = Json.utf8 + \\ helper = Str.trim + \\ + \\ # Test direct module access + \\ result1 = Json.parse + \\ + \\ # Test aliased module access + \\ result2 = Http.post + \\ + \\ # Test exposed items (should work without module prefix) + \\ result3 = get + \\ result4 = post + \\ + \\ # Test multiple qualified access + \\ combined = Str.concat( + \\ client, + \\ parser, + \\ helper, + \\ result1, + \\ result2, + \\ result3, + \\ result4, + \\ combined, + \\ ) + \\} + ; + + var test_env = try TestEnv.init("Test", source); + defer test_env.deinit(); + + const diagnostics = try test_env.module_env.getDiagnostics(); + defer test_env.gpa.free(diagnostics); + + // Count MODULE NOT FOUND errors + var module_not_found_count: usize = 0; + for (diagnostics) |diag| { + if (diag == .module_not_found) { + module_not_found_count += 1; + } + } + + // Qualified imports (json.Json, http.Client, utils.String) should NOT produce + // MODULE NOT FOUND errors - they're handled by the workspace resolver + try testing.expectEqual(@as(usize, 0), module_not_found_count); +} + +// Try with match and error propagation // + +test "check type - try return with match and error propagation should type-check" { + // This tests that a function returning Try(Str, _) with a wildcard error type + // should accept both error propagation (?) and explicit Err tags in match branches. + // The wildcard _ in the return type annotation should unify with any error type. + const source = + \\get_greeting : {} -> Try(Str, _) + \\get_greeting = |{}| { + \\ match 0 { + \\ 0 => Try.Ok(List.first(["hello"])?), + \\ _ => Err(Impossible) + \\ } + \\} + ; + // Expected: should pass type-checking with combined error type (open tag union) + try checkTypesModule(source, .{ .pass = .last_def }, "{ } -> Try(Str, [ListWasEmpty, Impossible, .._others2])"); +} + +test "check type - try operator on method call should apply to whole expression (#8646)" { + // Regression test for https://github.com/roc-lang/roc/issues/8646 + // The `?` suffix on `strings.first()` should apply to the entire method call expression, + // not just to the right side of the field access. Previously, the parser was attaching + // `?` to `first()` before creating the field_access node, causing a type mismatch error + // that expected `{ unknown: _field }`. + const source = + \\question_fail : List(Str) -> Try(Str, _) + \\question_fail = |strings| { + \\ first_str = strings.first()? + \\ Ok(first_str) + \\} + ; + try checkTypesModule(source, .{ .pass = .last_def }, "List(Str) -> Try(Str, [ListWasEmpty, ..others])"); +} + +// record extension in type annotations // + +test "check type - record extension - basic open record annotation" { + // Test that a function accepting { name: Str, ..others } can take records with extra fields + const source = + \\getName : { name: Str, ..others } -> Str + \\getName = |record| record.name + ; + try checkTypesModule(source, .{ .pass = .last_def }, "{ ..others, name: Str } -> Str"); +} + +test "check type - record extension - closed record satisfies open record" { + // A closed record { name: Str, age: I64 } should satisfy { name: Str, ..others } + const source = + \\getName : { name: Str, ..others } -> Str + \\getName = |record| record.name + \\ + \\result = getName({ name: "Alice", age: 30 }) + ; + try checkTypesModule(source, .{ .pass = .last_def }, "Str"); +} + +test "check type - record extension - multiple fields with extension" { + // Test with multiple required fields and an extension + const source = + \\getFullName : { first: Str, last: Str, ..others } -> Str + \\getFullName = |record| Str.concat(Str.concat(record.first, " "), record.last) + ; + try checkTypesModule(source, .{ .pass = .last_def }, "{ ..others, first: Str, last: Str } -> Str"); +} + +test "check type - record extension - nested records with extension" { + // Test record extension with nested record types + const source = + \\getPersonName : { person: { name: Str, ..inner }, ..outer } -> Str + \\getPersonName = |record| record.person.name + ; + try checkTypesModule(source, .{ .pass = .last_def }, "{ ..outer, person: { ..inner, name: Str } } -> Str"); +} + +test "check type - record extension - empty record with extension" { + // An empty record with extension means "any record" + const source = + \\takeAnyRecord : { ..others } -> Str + \\takeAnyRecord = |_record| "got a record" + ; + try checkTypesModule(source, .{ .pass = .last_def }, "{ ..others } -> Str"); +} + +test "check type - record extension - mismatch should fail" { + // Test that a record missing a required field should fail + const source = + \\getName : { name: Str, ..others } -> Str + \\getName = |record| record.name + \\ + \\result = getName({ age: 30 }) + ; + try checkTypesModule(source, .fail, "TYPE MISMATCH"); +} + +// List method syntax tests + +test "check type - List.get method syntax" { + // Check what type is inferred for [1].get(0) (this works at runtime) + const source = + \\result = [1].get(0) + ; + try checkTypesModule( + source, + .{ .pass = .last_def }, + \\Try(item, [OutOfBounds, ..others]) + \\ where [item.from_numeral : Numeral -> Try(item, [InvalidNumeral(Str)])] + , + ); +} + +// List.first method syntax tests - REGRESSION TEST for cycle detection bug + +test "check type - List.first method syntax should not create cyclic types" { + // REGRESSION TEST: This test reproduces a bug where calling [1].first() (method syntax) + // would cause an infinite loop in layout computation because the interpreter was creating + // cyclic rigid var mappings in the TypeScope when building layouts. + // + // The bug: method syntax creates a StaticDispatchConstraint on a flex var. + // When the return type is Try(item, [ListWasEmpty, ..others]) with an open tag union, + // the interpreter was creating cyclic rigid -> rigid mappings in the empty_scope TypeScope. + // + // Method syntax: [1].first() + // Should have same type as function syntax: List.first([1]) + // + // NOTE: The type checking itself is correct - this test verifies type checking produces + // the right type. The bug manifests in the interpreter's layout computation phase. + const source = + \\result = [1].first() + ; + // Expected: Try(item, [ListWasEmpty, ..others]) with item having from_numeral constraint + try checkTypesModule( + source, + .{ .pass = .last_def }, + \\Try(item, [ListWasEmpty, ..others]) + \\ where [item.from_numeral : Numeral -> Try(item, [InvalidNumeral(Str)])] + , + ); +} diff --git a/src/check/test/unify_test.zig b/src/check/test/unify_test.zig new file mode 100644 index 0000000000..77a78c2ed2 --- /dev/null +++ b/src/check/test/unify_test.zig @@ -0,0 +1,2092 @@ +//! TODO +const std = @import("std"); +const base = @import("base"); +const can = @import("can"); +const types_mod = @import("types"); + +const unify_mod = @import("../unify.zig"); +const problem_mod = @import("../problem.zig"); +const occurs = @import("../occurs.zig"); +const snapshot_mod = @import("../snapshot.zig"); + +const Region = base.Region; +const Ident = base.Ident; + +const ModuleEnv = can.ModuleEnv; + +const Scratch = unify_mod.Scratch; +const Result = unify_mod.Result; + +const Slot = types_mod.Slot; +const ResolvedVarDesc = types_mod.ResolvedVarDesc; +const ResolvedVarDescs = types_mod.ResolvedVarDescs; + +const TypeIdent = types_mod.TypeIdent; +const Var = types_mod.Var; +const Desc = types_mod.Descriptor; +const Rank = types_mod.Rank; +const Mark = types_mod.Mark; +const Flex = types_mod.Flex; +const Rigid = types_mod.Rigid; +const RecursionVar = types_mod.RecursionVar; +const Content = types_mod.Content; +const Alias = types_mod.Alias; +const NominalType = types_mod.NominalType; +const FlatType = types_mod.FlatType; +const Builtin = types_mod.Builtin; +const Tuple = types_mod.Tuple; +const Func = types_mod.Func; +const Record = types_mod.Record; +const RecordField = types_mod.RecordField; +const TwoRecordFields = types_mod.TwoRecordFields; +const TagUnion = types_mod.TagUnion; +const Tag = types_mod.Tag; +const TwoTags = types_mod.TwoTags; + +const VarSafeList = Var.SafeList; +const RecordFieldSafeMultiList = RecordField.SafeMultiList; +const RecordFieldSafeList = RecordField.SafeList; +const TwoRecordFieldsSafeMultiList = TwoRecordFields.SafeMultiList; +const TwoRecordFieldsSafeList = TwoRecordFields.SafeList; +const TagSafeList = Tag.SafeList; +const TagSafeMultiList = Tag.SafeMultiList; +const TwoTagsSafeList = TwoTags.SafeList; + +const Problem = problem_mod.Problem; + +/// A lightweight test harness used in unification and type inference tests. +/// +/// `TestEnv` bundles together the following components: +/// * a module env for holding things like idents +/// * a type store for registering and resolving types +/// * a reusable `Scratch` buffer for managing field partitions and temporary variables +/// +/// This is intended to simplify unit test setup, particularly for unifying records, +/// functions, aliases, and other structured types. +const TestEnv = struct { + const Self = @This(); + + module_env: *ModuleEnv, + snapshots: snapshot_mod.Store, + problems: problem_mod.Store, + type_writer: types_mod.TypeWriter, + scratch: Scratch, + occurs_scratch: occurs.Scratch, + + /// Init everything needed to test unify + /// This includes allocating module_env on the heap + /// + /// TODO: Is heap allocation unideal here? If we want to optimize tests, we + /// could pull module_env's initialization out of here, but this results in + /// slight more verbose setup for each test + fn init(gpa: std.mem.Allocator) std.mem.Allocator.Error!Self { + const module_env = try gpa.create(ModuleEnv); + module_env.* = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); + try module_env.initCIRFields("Test"); + return .{ + .module_env = module_env, + .snapshots = try snapshot_mod.Store.initCapacity(gpa, 16), + .problems = try problem_mod.Store.initCapacity(gpa, 16), + .type_writer = try types_mod.TypeWriter.initFromParts(gpa, &module_env.types, module_env.getIdentStore(), null), + .scratch = try Scratch.init(module_env.gpa), + .occurs_scratch = try occurs.Scratch.init(module_env.gpa), + }; + } + + /// Deinit the test env, including deallocing the module_env from the heap + fn deinit(self: *Self) void { + self.module_env.deinit(); + self.module_env.gpa.destroy(self.module_env); + self.snapshots.deinit(); + self.problems.deinit(self.module_env.gpa); + self.type_writer.deinit(); + self.scratch.deinit(); + self.occurs_scratch.deinit(); + } + + /// Helper function to call unify with args from TestEnv + fn unify(self: *Self, a: Var, b: Var) std.mem.Allocator.Error!Result { + return try unify_mod.unify( + self.module_env, + &self.module_env.types, + &self.problems, + &self.snapshots, + &self.type_writer, + &self.scratch, + &self.occurs_scratch, + a, + b, + ); + } + + const Error = error{ VarIsNotRoot, IsNotRecord, IsNotTagUnion }; + + /// Get a desc from a root var + fn getDescForRootVar(self: *Self, var_: Var) error{VarIsNotRoot}!Desc { + switch (self.module_env.types.getSlot(var_)) { + .root => |desc_idx| return self.module_env.types.getDesc(desc_idx), + .redirect => return error.VarIsNotRoot, + } + } + + /// Unwrap a record or throw + fn getRecordOrErr(desc: Desc) error{IsNotRecord}!Record { + return desc.content.unwrapRecord() orelse error.IsNotRecord; + } + + /// Unwrap a record or throw + fn getTagUnionOrErr(desc: Desc) error{IsNotTagUnion}!TagUnion { + return desc.content.unwrapTagUnion() orelse error.IsNotTagUnion; + } + + fn mkTypeIdent(self: *Self, name: []const u8) std.mem.Allocator.Error!TypeIdent { + const ident_idx = try self.module_env.getIdentStore().insert(self.module_env.gpa, Ident.for_text(name)); + return TypeIdent{ .ident_idx = ident_idx }; + } + + // helpers - rigid var // + + fn mkRigidVar(self: *Self, name: []const u8) std.mem.Allocator.Error!Content { + const ident_idx = try self.module_env.getIdentStore().insert(self.module_env.gpa, Ident.for_text(name)); + return Self.mkRigidVarFromIdent(ident_idx); + } + + fn mkRigidVarFromIdent(ident_idx: Ident.Idx) Content { + return .{ .rigid = Rigid.init(ident_idx) }; + } + + // helpers - alias // + + fn mkAlias(self: *Self, name: []const u8, backing_var: Var, args: []const Var) std.mem.Allocator.Error!Content { + return try self.module_env.types.mkAlias(try self.mkTypeIdent(name), backing_var, args); + } + + // helpers - structure - tuple // + + fn mkTuple(self: *Self, slice: []const Var) std.mem.Allocator.Error!Content { + const elems_range = try self.module_env.types.appendVars(slice); + return Content{ .structure = .{ .tuple = .{ .elems = elems_range } } }; + } + + // helpers - nominal type // + + fn mkNominalType(self: *Self, name: []const u8, backing_var: Var, args: []const Var) std.mem.Allocator.Error!Content { + return try self.module_env.types.mkNominal( + try self.mkTypeIdent(name), + backing_var, + args, + Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 0 }, + false, // Use nominal for tests + ); + } + + fn mkList(self: *Self, elem_var: Var) std.mem.Allocator.Error!Content { + return try self.mkNominalType("List", elem_var, &[_]Var{elem_var}); + } + + fn mkBox(self: *Self, elem_var: Var) std.mem.Allocator.Error!Content { + return try self.mkNominalType("Box", elem_var, &[_]Var{elem_var}); + } + + // helpers - structure - func // + + fn mkFuncPure(self: *Self, args: []const Var, ret: Var) std.mem.Allocator.Error!Content { + return try self.module_env.types.mkFuncPure(args, ret); + } + + fn mkFuncEffectful(self: *Self, args: []const Var, ret: Var) std.mem.Allocator.Error!Content { + return try self.module_env.types.mkFuncEffectful(args, ret); + } + + fn mkFuncUnbound(self: *Self, args: []const Var, ret: Var) std.mem.Allocator.Error!Content { + return try self.module_env.types.mkFuncUnbound(args, ret); + } + + fn mkFuncFlex(self: *Self, args: []const Var, ret: Var) std.mem.Allocator.Error!Content { + // For flex functions, we use unbound since we don't know the effectfulness yet + return try self.module_env.types.mkFuncUnbound(args, ret); + } + + // helpers - structure - records // + + fn mkRecordField(self: *Self, name: []const u8, var_: Var) std.mem.Allocator.Error!RecordField { + const ident_idx = try self.module_env.getIdentStore().insert(self.module_env.gpa, Ident.for_text(name)); + return Self.mkRecordFieldFromIdent(ident_idx, var_); + } + + fn mkRecordFieldFromIdent(ident_idx: Ident.Idx, var_: Var) RecordField { + return RecordField{ .name = ident_idx, .var_ = var_ }; + } + + const RecordInfo = struct { record: Record, content: Content }; + + fn mkRecord(self: *Self, fields: []const RecordField, ext_var: Var) std.mem.Allocator.Error!RecordInfo { + const fields_range = try self.module_env.types.appendRecordFields(fields); + const record = Record{ .fields = fields_range, .ext = ext_var }; + return .{ .content = Content{ .structure = .{ .record = record } }, .record = record }; + } + + fn mkRecordOpen(self: *Self, fields: []const RecordField) std.mem.Allocator.Error!RecordInfo { + const ext_var = try self.module_env.types.freshFromContent(.{ .flex = Flex.init() }); + return self.mkRecord(fields, ext_var); + } + + fn mkRecordClosed(self: *Self, fields: []const RecordField) std.mem.Allocator.Error!RecordInfo { + const ext_var = try self.module_env.types.freshFromContent(.{ .structure = .empty_record }); + return self.mkRecord(fields, ext_var); + } + + // helpers - structure - tag union // + + const TagUnionInfo = struct { tag_union: TagUnion, content: Content }; + + fn mkTagArgs(self: *Self, args: []const Var) std.mem.Allocator.Error!VarSafeList.Range { + return try self.module_env.types.appendVars(args); + } + + fn mkTag(self: *Self, name: []const u8, args: []const Var) std.mem.Allocator.Error!Tag { + const ident_idx = try self.module_env.getIdentStore().insert(self.module_env.gpa, Ident.for_text(name)); + return Tag{ .name = ident_idx, .args = try self.module_env.types.appendVars(args) }; + } + + fn mkTagUnion(self: *Self, tags: []const Tag, ext_var: Var) std.mem.Allocator.Error!TagUnionInfo { + const tags_range = try self.module_env.types.appendTags(tags); + const tag_union = TagUnion{ .tags = tags_range, .ext = ext_var }; + return .{ .content = Content{ .structure = .{ .tag_union = tag_union } }, .tag_union = tag_union }; + } + + fn mkTagUnionOpen(self: *Self, tags: []const Tag) std.mem.Allocator.Error!TagUnionInfo { + const ext_var = try self.module_env.types.freshFromContent(.{ .flex = Flex.init() }); + return self.mkTagUnion(tags, ext_var); + } + + fn mkTagUnionClosed(self: *Self, tags: []const Tag) std.mem.Allocator.Error!TagUnionInfo { + const ext_var = try self.module_env.types.freshFromContent(.{ .structure = .empty_tag_union }); + return self.mkTagUnion(tags, ext_var); + } +}; + +// unification - flex_vars // + +test "unify - identical" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const a = try env.module_env.types.fresh(); + const desc = try env.getDescForRootVar(a); + + const result = try env.unify(a, a); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(desc, try env.getDescForRootVar(a)); +} + +test "unify - both flex vars" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const a = try env.module_env.types.fresh(); + const b = try env.module_env.types.fresh(); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); +} + +// unification - rigid // + +test "rigid_var - unifies with flex_var" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const rigid = try env.mkRigidVar("a"); + const a = try env.module_env.types.freshFromContent(.{ .flex = Flex.init() }); + const b = try env.module_env.types.freshFromContent(rigid); + + const result = try env.unify(a, b); + try std.testing.expectEqual(true, result.isOk()); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + try std.testing.expectEqual(rigid, (try env.getDescForRootVar(b)).content); +} + +test "rigid_var - unifies with flex_var (other way)" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const rigid = try env.mkRigidVar("a"); + const a = try env.module_env.types.freshFromContent(rigid); + const b = try env.module_env.types.freshFromContent(.{ .flex = Flex.init() }); + + const result = try env.unify(a, b); + try std.testing.expectEqual(true, result.isOk()); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + try std.testing.expectEqual(rigid, (try env.getDescForRootVar(b)).content); +} + +test "rigid_var - cannot unify with alias (fail)" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const alias = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + const rigid = try env.module_env.types.freshFromContent(try env.mkRigidVar("a")); + + const result = try env.unify(alias, rigid); + try std.testing.expectEqual(false, result.isOk()); +} + +test "rigid_var - cannot unify with identical ident str (fail)" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const rigid1 = try env.module_env.types.freshFromContent(try env.mkRigidVar("a")); + const rigid2 = try env.module_env.types.freshFromContent(try env.mkRigidVar("a")); + + const result = try env.unify(rigid1, rigid2); + try std.testing.expectEqual(false, result.isOk()); +} +// unification - aliases // + +test "unify - aliases with different names but same backing" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + // Create alias `a` with its backing var and arg + const a_backing_var = try env.module_env.types.freshFromContent(try env.mkTuple(&[_]Var{str})); + const a_alias = try env.mkAlias("AliasA", a_backing_var, &[_]Var{str}); + const a = try env.module_env.types.freshFromContent(a_alias); + + // Create alias `b` with its backing var and arg + const b_backing_var = try env.module_env.types.freshFromContent(try env.mkTuple(&[_]Var{str})); + const b_alias = try env.mkAlias("AliasB", b_backing_var, &[_]Var{str}); + const b = try env.module_env.types.freshFromContent(b_alias); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(a_alias, (try env.getDescForRootVar(a)).content); + try std.testing.expectEqual(b_alias, (try env.getDescForRootVar(b)).content); +} + +test "unify - alias with concrete" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const a_backing_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + const a_alias = try env.mkAlias("Alias", a_backing_var, &[_]Var{}); + + const a = try env.module_env.types.freshFromContent(a_alias); + const b = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + + // Assert that the alias was preserved + const resolved = env.module_env.types.resolveVar(a); + try std.testing.expect(resolved.desc.content == .alias); + + // Assert that the alias backing var was preserved + const resolved_backing = env.module_env.types.resolveVar( + env.module_env.types.getAliasBackingVar(resolved.desc.content.alias), + ); + try std.testing.expectEqual(Content{ .structure = .empty_record }, resolved_backing.desc.content); + + // Assert that a & b redirect to the alias + try std.testing.expectEqual(Slot{ .redirect = resolved.var_ }, env.module_env.types.getSlot(a)); + try std.testing.expectEqual(Slot{ .redirect = resolved.var_ }, env.module_env.types.getSlot(b)); +} + +test "unify - alias with concrete other way" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const b_backing_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + const b_alias = try env.mkAlias("Alias", b_backing_var, &[_]Var{}); + + const a = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + const b = try env.module_env.types.freshFromContent(b_alias); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + + // Assert that the alias was preserved + const resolved = env.module_env.types.resolveVar(a); + try std.testing.expect(resolved.desc.content == .alias); + + // Assert that the alias backing var was preserved + const resolved_backing = env.module_env.types.resolveVar( + env.module_env.types.getAliasBackingVar(resolved.desc.content.alias), + ); + try std.testing.expectEqual(Content{ .structure = .empty_record }, resolved_backing.desc.content); + + // Assert that a & b redirect to the alias + try std.testing.expectEqual(Slot{ .redirect = resolved.var_ }, env.module_env.types.getSlot(a)); + try std.testing.expectEqual(Slot{ .redirect = resolved.var_ }, env.module_env.types.getSlot(b)); +} + +// unification - structure/flex_vars // + +test "unify - a is builtin and b is flex_var" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = Content{ .structure = .empty_record }; + + const a = try env.module_env.types.freshFromContent(str); + const b = try env.module_env.types.fresh(); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + try std.testing.expectEqual(str, (try env.getDescForRootVar(b)).content); +} + +test "unify - a is flex_var and b is builtin" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = Content{ .structure = .empty_record }; + + const a = try env.module_env.types.fresh(); + const b = try env.module_env.types.freshFromContent(str); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + try std.testing.expectEqual(str, (try env.getDescForRootVar(b)).content); +} + +// unification - structure/structure - builtin // + +test "unify - a & b are both str" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = Content{ .structure = .empty_record }; + + const a = try env.module_env.types.freshFromContent(str); + const b = try env.module_env.types.freshFromContent(str); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + try std.testing.expectEqual(str, (try env.getDescForRootVar(b)).content); +} + +test "unify - a & b box with same arg unify" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = Content{ .structure = .empty_record }; + const str_var = try env.module_env.types.freshFromContent(str); + + const box_str = try env.mkBox(str_var); + + const a = try env.module_env.types.freshFromContent(box_str); + const b = try env.module_env.types.freshFromContent(box_str); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + try std.testing.expectEqual(box_str, (try env.getDescForRootVar(b)).content); +} + +test "unify - a & b list with same arg unify" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = Content{ .structure = .empty_record }; + const str_var = try env.module_env.types.freshFromContent(str); + + const list_str = try env.mkList(str_var); + + const a = try env.module_env.types.freshFromContent(list_str); + const b = try env.module_env.types.freshFromContent(list_str); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + try std.testing.expectEqual(list_str, (try env.getDescForRootVar(b)).content); +} +// unification - structure/structure - tuple // +// unification - structure/structure - poly/compact_int // +// unification - structure/structure - poly/compact_frac // +// unification - structure/structure - poly/poly rigid // +// unification - structure/structure - poly/compact rigid // +// unification - structure/structure - func // + +test "unify - first is flex, second is func" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const tag_payload = try env.module_env.types.fresh(); + const tag = try env.mkTag("Some", &[_]Var{tag_payload}); + const backing_var = try env.module_env.types.freshFromContent((try env.mkTagUnionOpen(&[_]Tag{tag})).content); + const nominal_type = try env.module_env.types.freshFromContent(try env.mkNominalType("List", backing_var, &[_]Var{})); + const arg = try env.module_env.types.fresh(); + const func = try env.mkFuncUnbound(&[_]Var{arg}, nominal_type); + + const a = try env.module_env.types.fresh(); + const b = try env.module_env.types.freshFromContent(func); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(true, result.isOk()); +} +// unification - structure/structure - nominal type // + +// unification - nominal types with anonymous tag unions // + +test "unify - anonymous tag union unifies with nominal tag union (nominal on left)" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create nominal type: Foo := [A(Str), B] + const str_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + const tag_a = try env.mkTag("A", &[_]Var{str_var}); + const tag_b = try env.mkTag("B", &[_]Var{}); + const backing_tu = try env.mkTagUnionClosed(&[_]Tag{ tag_a, tag_b }); + const backing_var = try env.module_env.types.freshFromContent(backing_tu.content); + const nominal_var = try env.module_env.types.freshFromContent( + try env.mkNominalType("Foo", backing_var, &[_]Var{}), + ); + + // Create anonymous tag union: [A(Str)] + const anon_tag_a = try env.mkTag("A", &[_]Var{str_var}); + const anon_tu = try env.mkTagUnionOpen(&[_]Tag{anon_tag_a}); + const anon_var = try env.module_env.types.freshFromContent(anon_tu.content); + + // Unify: Foo ~ [A(Str)] + const result = try env.unify(nominal_var, anon_var); + + // Should succeed and merge to nominal type + try std.testing.expectEqual(.ok, result); + const resolved = env.module_env.types.resolveVar(anon_var); + try std.testing.expect(resolved.desc.content == .structure); + try std.testing.expect(resolved.desc.content.structure == .nominal_type); +} + +test "unify - anonymous tag union unifies with nominal (nominal on right)" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create nominal type: Foo := [A, B, C] + const tag_a = try env.mkTag("A", &[_]Var{}); + const tag_b = try env.mkTag("B", &[_]Var{}); + const tag_c = try env.mkTag("C", &[_]Var{}); + const backing_tu = try env.mkTagUnionClosed(&[_]Tag{ tag_a, tag_b, tag_c }); + const backing_var = try env.module_env.types.freshFromContent(backing_tu.content); + const nominal_var = try env.module_env.types.freshFromContent( + try env.mkNominalType("Foo", backing_var, &[_]Var{}), + ); + + // Create anonymous tag union: [B] + const anon_tag_b = try env.mkTag("B", &[_]Var{}); + const anon_tu = try env.mkTagUnionOpen(&[_]Tag{anon_tag_b}); + const anon_var = try env.module_env.types.freshFromContent(anon_tu.content); + + // Unify: [B] ~ Foo (swapped order) + const result = try env.unify(anon_var, nominal_var); + + // Should succeed and merge to nominal type + try std.testing.expectEqual(.ok, result); + const resolved = env.module_env.types.resolveVar(anon_var); + try std.testing.expect(resolved.desc.content == .structure); + try std.testing.expect(resolved.desc.content.structure == .nominal_type); +} + +test "unify - anonymous tag union with wrong tag fails" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create nominal type: Foo := [A, B] + const tag_a = try env.mkTag("A", &[_]Var{}); + const tag_b = try env.mkTag("B", &[_]Var{}); + const backing_tu = try env.mkTagUnionClosed(&[_]Tag{ tag_a, tag_b }); + const backing_var = try env.module_env.types.freshFromContent(backing_tu.content); + const nominal_var = try env.module_env.types.freshFromContent( + try env.mkNominalType("Foo", backing_var, &[_]Var{}), + ); + + // Create anonymous tag union: [D] (D doesn't exist in Foo) + const anon_tag_d = try env.mkTag("D", &[_]Var{}); + const anon_tu = try env.mkTagUnionOpen(&[_]Tag{anon_tag_d}); + const anon_var = try env.module_env.types.freshFromContent(anon_tu.content); + + // Unify: Foo ~ [D] - should fail + const result = try env.unify(nominal_var, anon_var); + + // Should fail + try std.testing.expectEqual(false, result.isOk()); +} + +test "unify - anonymous tag union with multiple tags unifies" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create nominal type: Foo := [A, B, C] + const tag_a = try env.mkTag("A", &[_]Var{}); + const tag_b = try env.mkTag("B", &[_]Var{}); + const tag_c = try env.mkTag("C", &[_]Var{}); + const backing_tu = try env.mkTagUnionClosed(&[_]Tag{ tag_a, tag_b, tag_c }); + const backing_var = try env.module_env.types.freshFromContent(backing_tu.content); + const nominal_var = try env.module_env.types.freshFromContent( + try env.mkNominalType("Foo", backing_var, &[_]Var{}), + ); + + // Create anonymous tag union: [A, B] + const anon_tag_a = try env.mkTag("A", &[_]Var{}); + const anon_tag_b = try env.mkTag("B", &[_]Var{}); + const anon_tu = try env.mkTagUnionOpen(&[_]Tag{ anon_tag_a, anon_tag_b }); + const anon_var = try env.module_env.types.freshFromContent(anon_tu.content); + + // Unify: Foo ~ [A, B] + const result = try env.unify(nominal_var, anon_var); + + // Should succeed + try std.testing.expectEqual(.ok, result); + const resolved = env.module_env.types.resolveVar(anon_var); + try std.testing.expect(resolved.desc.content == .structure); + try std.testing.expect(resolved.desc.content.structure == .nominal_type); +} + +// unification - empty nominal types // + +test "unify - empty nominal type with empty tag union (nominal on left)" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create nominal type: Empty := [] + const backing_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_tag_union }); + const nominal_var = try env.module_env.types.freshFromContent( + try env.mkNominalType("Empty", backing_var, &[_]Var{}), + ); + + // Create empty tag union: [] + const empty_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_tag_union }); + + // Unify: Empty ~ [] + const result = try env.unify(nominal_var, empty_var); + + // Should succeed and merge to nominal type + try std.testing.expectEqual(.ok, result); + const resolved = env.module_env.types.resolveVar(empty_var); + try std.testing.expect(resolved.desc.content == .structure); + try std.testing.expect(resolved.desc.content.structure == .nominal_type); +} + +test "unify - empty tag union with empty nominal type (nominal on right)" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create empty tag union: [] + const empty_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_tag_union }); + + // Create nominal type: Empty := [] + const backing_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_tag_union }); + const nominal_var = try env.module_env.types.freshFromContent( + try env.mkNominalType("Empty", backing_var, &[_]Var{}), + ); + + // Unify: [] ~ Empty + const result = try env.unify(empty_var, nominal_var); + + // Should succeed and merge to nominal type + try std.testing.expectEqual(.ok, result); + const resolved = env.module_env.types.resolveVar(empty_var); + try std.testing.expect(resolved.desc.content == .structure); + try std.testing.expect(resolved.desc.content.structure == .nominal_type); +} + +test "unify - two empty nominal types" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create nominal type: Empty1 := [] + const backing_var1 = try env.module_env.types.freshFromContent(Content{ .structure = .empty_tag_union }); + const nominal_var1 = try env.module_env.types.freshFromContent( + try env.mkNominalType("Empty1", backing_var1, &[_]Var{}), + ); + + // Create nominal type: Empty2 := [] + const backing_var2 = try env.module_env.types.freshFromContent(Content{ .structure = .empty_tag_union }); + const nominal_var2 = try env.module_env.types.freshFromContent( + try env.mkNominalType("Empty2", backing_var2, &[_]Var{}), + ); + + // Unify: Empty1 ~ Empty2 - should fail (different nominal types) + const result = try env.unify(nominal_var1, nominal_var2); + + // Should fail because they're different nominal types + try std.testing.expectEqual(false, result.isOk()); +} + +test "unify - empty nominal type with non-empty tag union fails" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create nominal type: Empty := [] + const backing_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_tag_union }); + const nominal_var = try env.module_env.types.freshFromContent( + try env.mkNominalType("Empty", backing_var, &[_]Var{}), + ); + + // Create non-empty tag union: [A] + const anon_tag_a = try env.mkTag("A", &[_]Var{}); + const anon_tu = try env.mkTagUnionOpen(&[_]Tag{anon_tag_a}); + const anon_var = try env.module_env.types.freshFromContent(anon_tu.content); + + // Unify: Empty ~ [A] - should fail + const result = try env.unify(nominal_var, anon_var); + + // Should fail + try std.testing.expectEqual(false, result.isOk()); +} + +// unification - records - partition fields // + +test "partitionFields - same record" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const var_x = try env.module_env.types.fresh(); + const var_y = try env.module_env.types.fresh(); + const field_x = try env.mkRecordField("field_x", var_x); + const field_y = try env.mkRecordField("field_y", var_y); + + const range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{ field_x, field_y }); + + const result = try unify_mod.partitionFields(env.module_env.getIdentStore(), &env.scratch, range, range); + + try std.testing.expectEqual(0, result.only_in_a.len()); + try std.testing.expectEqual(0, result.only_in_b.len()); + try std.testing.expectEqual(2, result.in_both.len()); + + const both_slice = env.scratch.in_both_fields.sliceRange(result.in_both); + try std.testing.expectEqual(field_x, both_slice[0].a); + try std.testing.expectEqual(field_x, both_slice[0].b); + try std.testing.expectEqual(field_y, both_slice[1].a); + try std.testing.expectEqual(field_y, both_slice[1].b); +} + +test "partitionFields - disjoint fields" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const var_a1 = try env.module_env.types.fresh(); + const var_a2 = try env.module_env.types.fresh(); + const var_b1 = try env.module_env.types.fresh(); + const a1 = try env.mkRecordField("a1", var_a1); + const a2 = try env.mkRecordField("a2", var_a2); + const b1 = try env.mkRecordField("b1", var_b1); + + const a_range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{ a1, a2 }); + const b_range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{b1}); + + const result = try unify_mod.partitionFields(env.module_env.getIdentStore(), &env.scratch, a_range, b_range); + + try std.testing.expectEqual(2, result.only_in_a.len()); + try std.testing.expectEqual(1, result.only_in_b.len()); + try std.testing.expectEqual(0, result.in_both.len()); + + const only_in_a_slice = env.scratch.only_in_a_fields.sliceRange(result.only_in_a); + try std.testing.expectEqual(a1, only_in_a_slice[0]); + try std.testing.expectEqual(a2, only_in_a_slice[1]); + + const only_in_b_slice = env.scratch.only_in_b_fields.sliceRange(result.only_in_b); + try std.testing.expectEqual(b1, only_in_b_slice[0]); +} + +test "partitionFields - overlapping fields" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const var_a1 = try env.module_env.types.fresh(); + const var_both = try env.module_env.types.fresh(); + const var_b1 = try env.module_env.types.fresh(); + const a1 = try env.mkRecordField("a1", var_a1); + const both = try env.mkRecordField("both", var_both); + const b1 = try env.mkRecordField("b1", var_b1); + + const a_range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{ a1, both }); + const b_range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{ b1, both }); + + const result = try unify_mod.partitionFields(env.module_env.getIdentStore(), &env.scratch, a_range, b_range); + + try std.testing.expectEqual(1, result.only_in_a.len()); + try std.testing.expectEqual(1, result.only_in_b.len()); + try std.testing.expectEqual(1, result.in_both.len()); + + const both_slice = env.scratch.in_both_fields.sliceRange(result.in_both); + try std.testing.expectEqual(both, both_slice[0].a); + try std.testing.expectEqual(both, both_slice[0].b); + + const only_in_a_slice = env.scratch.only_in_a_fields.sliceRange(result.only_in_a); + try std.testing.expectEqual(a1, only_in_a_slice[0]); + + const only_in_b_slice = env.scratch.only_in_b_fields.sliceRange(result.only_in_b); + try std.testing.expectEqual(b1, only_in_b_slice[0]); +} + +test "partitionFields - reordering is normalized" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const var_f1 = try env.module_env.types.fresh(); + const var_f2 = try env.module_env.types.fresh(); + const var_f3 = try env.module_env.types.fresh(); + const f1 = try env.mkRecordField("f1", var_f1); + const f2 = try env.mkRecordField("f2", var_f2); + const f3 = try env.mkRecordField("f3", var_f3); + + const a_range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{ f3, f1, f2 }); + const b_range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{ f1, f2, f3 }); + + const result = try unify_mod.partitionFields(env.module_env.getIdentStore(), &env.scratch, a_range, b_range); + + try std.testing.expectEqual(0, result.only_in_a.len()); + try std.testing.expectEqual(0, result.only_in_b.len()); + try std.testing.expectEqual(3, result.in_both.len()); + + const both = env.scratch.in_both_fields.sliceRange(result.in_both); + try std.testing.expectEqual(f1, both[0].a); + try std.testing.expectEqual(f1, both[0].b); + try std.testing.expectEqual(f2, both[1].a); + try std.testing.expectEqual(f2, both[1].b); + try std.testing.expectEqual(f3, both[2].a); + try std.testing.expectEqual(f3, both[2].b); +} + +// unification - structure/structure - records closed // + +test "unify - identical closed records" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const fields = [_]RecordField{try env.mkRecordField("a", str)}; + const record_data = try env.mkRecordClosed(&fields); + const record_data_fields = env.module_env.types.record_fields.sliceRange(record_data.record.fields); + + const a = try env.module_env.types.freshFromContent(record_data.content); + const b = try env.module_env.types.freshFromContent(record_data.content); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + + const b_record = try TestEnv.getRecordOrErr(try env.getDescForRootVar(b)); + const b_record_fields = env.module_env.types.record_fields.sliceRange(b_record.fields); + try std.testing.expectEqualSlices(Ident.Idx, record_data_fields.items(.name), b_record_fields.items(.name)); + try std.testing.expectEqualSlices(Var, record_data_fields.items(.var_), b_record_fields.items(.var_)); +} + +test "unify - closed record mismatch on diff fields (fail)" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const field1 = try env.mkRecordField("field1", str); + const field2 = try env.mkRecordField("field2", str); + + const a_record_data = try env.mkRecordClosed(&[_]RecordField{ field1, field2 }); + const a = try env.module_env.types.freshFromContent(a_record_data.content); + + const b_record_data = try env.mkRecordClosed(&[_]RecordField{field1}); + const b = try env.module_env.types.freshFromContent(b_record_data.content); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(false, result.isOk()); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + + const desc_b = try env.getDescForRootVar(b); + try std.testing.expectEqual(Content.err, desc_b.content); +} + +// unification - structure/structure - records open // + +test "unify - identical open records" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const field_shared = try env.mkRecordField("x", str); + + const a_rec_data = try env.mkRecordOpen(&[_]RecordField{field_shared}); + const a = try env.module_env.types.freshFromContent(a_rec_data.content); + const b_rec_data = try env.mkRecordOpen(&[_]RecordField{field_shared}); + const b = try env.module_env.types.freshFromContent(b_rec_data.content); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + + // check that the update var at b is correct + + const b_record = try TestEnv.getRecordOrErr(try env.getDescForRootVar(b)); + try std.testing.expectEqual(1, b_record.fields.len()); + const b_record_fields = env.module_env.types.getRecordFieldsSlice(b_record.fields); + try std.testing.expectEqual(field_shared.name, b_record_fields.items(.name)[0]); + try std.testing.expectEqual(field_shared.var_, b_record_fields.items(.var_)[0]); + + const b_ext = env.module_env.types.resolveVar(b_record.ext).desc.content; + try std.testing.expectEqual(Content{ .flex = Flex.init() }, b_ext); + + // check that fresh vars are correct + + try std.testing.expectEqual(0, env.scratch.fresh_vars.len()); +} + +// unification - structure/structure - records open+closed // + +test "unify - open record extends closed (fail)" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const field_x = try env.mkRecordField("field_x", str); + const field_y = try env.mkRecordField("field_y", str); + + const open = try env.module_env.types.freshFromContent((try env.mkRecordOpen(&[_]RecordField{ field_x, field_y })).content); + const closed = try env.module_env.types.freshFromContent((try env.mkRecordClosed(&[_]RecordField{field_x})).content); + + const result = try env.unify(open, closed); + + try std.testing.expectEqual(false, result.isOk()); + try std.testing.expectEqual(Slot{ .redirect = closed }, env.module_env.types.getSlot(open)); + try std.testing.expectEqual(Content.err, (try env.getDescForRootVar(closed)).content); +} + +test "unify - closed record extends open" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const field_x = try env.mkRecordField("field_x", str); + const field_y = try env.mkRecordField("field_y", str); + + const open = try env.module_env.types.freshFromContent((try env.mkRecordOpen(&[_]RecordField{field_x})).content); + const closed = try env.module_env.types.freshFromContent((try env.mkRecordClosed(&[_]RecordField{ field_x, field_y })).content); + + const result = try env.unify(open, closed); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(Slot{ .redirect = closed }, env.module_env.types.getSlot(open)); +} + +// unification - tag unions - partition tags // + +test "partitionTags - same tags" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const var_x = try env.module_env.types.fresh(); + const var_y = try env.module_env.types.fresh(); + const tag_x = try env.mkTag("X", &[_]Var{var_x}); + const tag_y = try env.mkTag("Y", &[_]Var{var_y}); + + const range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ tag_x, tag_y }); + + const result = try unify_mod.partitionTags(env.module_env.getIdentStore(), &env.scratch, range, range); + + try std.testing.expectEqual(0, result.only_in_a.len()); + try std.testing.expectEqual(0, result.only_in_b.len()); + try std.testing.expectEqual(2, result.in_both.len()); + + const both_slice = env.scratch.in_both_tags.sliceRange(result.in_both); + try std.testing.expectEqual(tag_x, both_slice[0].a); + try std.testing.expectEqual(tag_x, both_slice[0].b); + try std.testing.expectEqual(tag_y, both_slice[1].a); + try std.testing.expectEqual(tag_y, both_slice[1].b); +} + +test "partitionTags - disjoint fields" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const var_a1 = try env.module_env.types.fresh(); + const var_a2 = try env.module_env.types.fresh(); + const var_b1 = try env.module_env.types.fresh(); + const a1 = try env.mkTag("A1", &[_]Var{var_a1}); + const a2 = try env.mkTag("A2", &[_]Var{var_a2}); + const b1 = try env.mkTag("B1", &[_]Var{var_b1}); + + const a_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ a1, a2 }); + const b_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{b1}); + + const result = try unify_mod.partitionTags(env.module_env.getIdentStore(), &env.scratch, a_range, b_range); + + try std.testing.expectEqual(2, result.only_in_a.len()); + try std.testing.expectEqual(1, result.only_in_b.len()); + try std.testing.expectEqual(0, result.in_both.len()); + + const only_in_a_slice = env.scratch.only_in_a_tags.sliceRange(result.only_in_a); + try std.testing.expectEqual(a1, only_in_a_slice[0]); + try std.testing.expectEqual(a2, only_in_a_slice[1]); + + const only_in_b_slice = env.scratch.only_in_b_tags.sliceRange(result.only_in_b); + try std.testing.expectEqual(b1, only_in_b_slice[0]); +} + +test "partitionTags - overlapping tags" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const var_a = try env.module_env.types.fresh(); + const var_both = try env.module_env.types.fresh(); + const var_b = try env.module_env.types.fresh(); + const a1 = try env.mkTag("A", &[_]Var{var_a}); + const both = try env.mkTag("Both", &[_]Var{var_both}); + const b1 = try env.mkTag("B", &[_]Var{var_b}); + + const a_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ a1, both }); + const b_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ b1, both }); + + const result = try unify_mod.partitionTags(env.module_env.getIdentStore(), &env.scratch, a_range, b_range); + + try std.testing.expectEqual(1, result.only_in_a.len()); + try std.testing.expectEqual(1, result.only_in_b.len()); + try std.testing.expectEqual(1, result.in_both.len()); + + const both_slice = env.scratch.in_both_tags.sliceRange(result.in_both); + try std.testing.expectEqual(both, both_slice[0].a); + try std.testing.expectEqual(both, both_slice[0].b); + + const only_in_a_slice = env.scratch.only_in_a_tags.sliceRange(result.only_in_a); + try std.testing.expectEqual(a1, only_in_a_slice[0]); + + const only_in_b_slice = env.scratch.only_in_b_tags.sliceRange(result.only_in_b); + try std.testing.expectEqual(b1, only_in_b_slice[0]); +} + +test "partitionTags - reordering is normalized" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const var_f1 = try env.module_env.types.fresh(); + const var_f2 = try env.module_env.types.fresh(); + const var_f3 = try env.module_env.types.fresh(); + const f1 = try env.mkTag("F1", &[_]Var{var_f1}); + const f2 = try env.mkTag("F2", &[_]Var{var_f2}); + const f3 = try env.mkTag("F3", &[_]Var{var_f3}); + + const a_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ f3, f1, f2 }); + const b_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ f1, f2, f3 }); + + const result = try unify_mod.partitionTags(env.module_env.getIdentStore(), &env.scratch, a_range, b_range); + + try std.testing.expectEqual(0, result.only_in_a.len()); + try std.testing.expectEqual(0, result.only_in_b.len()); + try std.testing.expectEqual(3, result.in_both.len()); + + const both_slice = env.scratch.in_both_tags.sliceRange(result.in_both); + try std.testing.expectEqual(f1, both_slice[0].a); + try std.testing.expectEqual(f1, both_slice[0].b); + try std.testing.expectEqual(f2, both_slice[1].a); + try std.testing.expectEqual(f2, both_slice[1].b); + try std.testing.expectEqual(f3, both_slice[2].a); + try std.testing.expectEqual(f3, both_slice[2].b); +} + +// unification - structure/structure - tag unions closed // + +test "unify - identical closed tag_unions" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const tag = try env.mkTag("A", &[_]Var{str}); + const tags = [_]Tag{tag}; + const tag_union_data = try env.mkTagUnionClosed(&tags); + + const a = try env.module_env.types.freshFromContent(tag_union_data.content); + const b = try env.module_env.types.freshFromContent(tag_union_data.content); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + + const b_tag_union = try TestEnv.getTagUnionOrErr(try env.getDescForRootVar(b)); + const b_tags = env.module_env.types.tags.sliceRange(b_tag_union.tags); + const b_tags_names = b_tags.items(.name); + const b_tags_args = b_tags.items(.args); + try std.testing.expectEqual(1, b_tags.len); + try std.testing.expectEqual(tag.name, b_tags_names[0]); + try std.testing.expectEqual(tag.args, b_tags_args[0]); + + try std.testing.expectEqual(1, b_tags.len); + + const b_tag_args = env.module_env.types.vars.sliceRange(b_tags_args[0]); + try std.testing.expectEqual(1, b_tag_args.len); + try std.testing.expectEqual(str, b_tag_args[0]); +} + +// unification - structure/structure - tag unions open // + +test "unify - identical open tag unions" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const tag_shared = try env.mkTag("Shared", &[_]Var{ str, str }); + + const tag_union_a = try env.mkTagUnionOpen(&[_]Tag{tag_shared}); + const a = try env.module_env.types.freshFromContent(tag_union_a.content); + + const tag_union_b = try env.mkTagUnionOpen(&[_]Tag{tag_shared}); + const b = try env.module_env.types.freshFromContent(tag_union_b.content); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + + // check that the update var at b is correct + + const b_tag_union = try TestEnv.getTagUnionOrErr(try env.getDescForRootVar(b)); + try std.testing.expectEqual(1, b_tag_union.tags.len()); + + const b_tags = env.module_env.types.tags.sliceRange(b_tag_union.tags); + const b_tags_names = b_tags.items(.name); + const b_tags_args = b_tags.items(.args); + try std.testing.expectEqual(1, b_tags.len); + try std.testing.expectEqual(tag_shared.name, b_tags_names[0]); + try std.testing.expectEqual(tag_shared.args, b_tags_args[0]); + + const b_ext = env.module_env.types.resolveVar(b_tag_union.ext).desc.content; + try std.testing.expectEqual(Content{ .flex = Flex.init() }, b_ext); + + // check that fresh vars are correct + + try std.testing.expectEqual(0, env.scratch.fresh_vars.len()); +} + +// unification - structure/structure - records open+closed // + +test "unify - open tag extends closed (fail)" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const tag_shared = try env.mkTag("Shared", &[_]Var{str}); + const tag_a_only = try env.mkTag("A", &[_]Var{str}); + + const a = try env.module_env.types.freshFromContent((try env.mkTagUnionOpen(&[_]Tag{ tag_shared, tag_a_only })).content); + const b = try env.module_env.types.freshFromContent((try env.mkTagUnionClosed(&[_]Tag{tag_shared})).content); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(false, result.isOk()); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + try std.testing.expectEqual(Content.err, (try env.getDescForRootVar(b)).content); +} + +test "unify - closed tag union extends open" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const tag_shared = try env.mkTag("Shared", &[_]Var{str}); + const tag_b_only = try env.mkTag("B", &[_]Var{str}); + + const a = try env.module_env.types.freshFromContent((try env.mkTagUnionOpen(&[_]Tag{tag_shared})).content); + const b = try env.module_env.types.freshFromContent((try env.mkTagUnionClosed(&[_]Tag{ tag_shared, tag_b_only })).content); + + const result = try env.unify(a, b); + + try std.testing.expectEqual(.ok, result); + try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); + + // check that the update var at b is correct + + const b_tag_union = try TestEnv.getTagUnionOrErr(try env.getDescForRootVar(b)); + try std.testing.expectEqual(1, b_tag_union.tags.len()); + + const b_tags = env.module_env.types.tags.sliceRange(b_tag_union.tags); + const b_tags_names = b_tags.items(.name); + const b_tags_args = b_tags.items(.args); + try std.testing.expectEqual(1, b_tags.len); + try std.testing.expectEqual(tag_shared.name, b_tags_names[0]); + try std.testing.expectEqual(tag_shared.args, b_tags_args[0]); + + const b_ext_tag_union = try TestEnv.getTagUnionOrErr(env.module_env.types.resolveVar(b_tag_union.ext).desc); + try std.testing.expectEqual(1, b_ext_tag_union.tags.len()); + + const b_ext_tags = env.module_env.types.tags.sliceRange(b_ext_tag_union.tags); + const b_ext_tags_names = b_ext_tags.items(.name); + const b_ext_tags_args = b_ext_tags.items(.args); + try std.testing.expectEqual(1, b_ext_tags.len); + try std.testing.expectEqual(tag_b_only.name, b_ext_tags_names[0]); + try std.testing.expectEqual(tag_b_only.args, b_ext_tags_args[0]); + + const b_ext_ext = env.module_env.types.resolveVar(b_ext_tag_union.ext).desc.content; + try std.testing.expectEqual(Content{ .structure = .empty_tag_union }, b_ext_ext); + + // check that fresh vars are correct + + try std.testing.expectEqual(1, env.scratch.fresh_vars.len()); + try std.testing.expectEqual(b_tag_union.ext, env.scratch.fresh_vars.items.items[0]); +} + +// unification - recursion // + +test "unify - fails on infinite type" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const a = try env.module_env.types.fresh(); + const a_elems_range = try env.module_env.types.appendVars(&[_]Var{ a, str_var }); + const a_tuple = types_mod.Tuple{ .elems = a_elems_range }; + try env.module_env.types.setRootVarContent(a, Content{ .structure = .{ .tuple = a_tuple } }); + + const b = try env.module_env.types.fresh(); + const b_elems_range = try env.module_env.types.appendVars(&[_]Var{ b, str_var }); + const b_tuple = types_mod.Tuple{ .elems = b_elems_range }; + try env.module_env.types.setRootVarContent(b, Content{ .structure = .{ .tuple = b_tuple } }); + + const result = try env.unify(a, b); + + switch (result) { + .ok => try std.testing.expect(false), + .problem => |problem_idx| { + const problem = env.problems.get(problem_idx); + try std.testing.expectEqual(.infinite_recursion, @as(Problem.Tag, problem)); + + // Verify that a snapshot was created for the recursion error + const snapshot_idx = problem.infinite_recursion.snapshot; + const snapshot_content = env.snapshots.getContent(snapshot_idx); + // The snapshot should be some valid content (not just err) + try std.testing.expect(snapshot_content != .err); + + // Verify a formatted string was created + const formatted = env.snapshots.getFormattedString(snapshot_idx); + try std.testing.expect(formatted != null); + }, + } +} + +test "unify - fails on anonymous recursion" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create a tag union that recursively contains itself (anonymous recursion) + // This is like: a = [A a] unifying with b = [A b] + const tag_var_a = try env.module_env.types.fresh(); + const tag_a = try env.mkTag("A", &[_]Var{tag_var_a}); + const tag_union_a = try env.mkTagUnionClosed(&[_]Tag{tag_a}); + try env.module_env.types.setRootVarContent(tag_var_a, tag_union_a.content); + + const tag_var_b = try env.module_env.types.fresh(); + const tag_b = try env.mkTag("A", &[_]Var{tag_var_b}); + const tag_union_b = try env.mkTagUnionClosed(&[_]Tag{tag_b}); + try env.module_env.types.setRootVarContent(tag_var_b, tag_union_b.content); + + const result = try env.unify(tag_var_a, tag_var_b); + + switch (result) { + .ok => try std.testing.expect(false), + .problem => |problem_idx| { + const problem = env.problems.get(problem_idx); + try std.testing.expectEqual(.anonymous_recursion, @as(Problem.Tag, problem)); + + // Verify that a snapshot was created for the recursion error + const snapshot_idx = problem.anonymous_recursion.snapshot; + const snapshot_content = env.snapshots.getContent(snapshot_idx); + // The snapshot should be some valid content (not just err) + try std.testing.expect(snapshot_content != .err); + + // Verify a formatted string was created + const formatted = env.snapshots.getFormattedString(snapshot_idx); + try std.testing.expect(formatted != null); + }, + } +} + +test "unify - succeeds on nominal, tag union recursion" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + var types_store = &env.module_env.types; + + // Create vars in the required order for adjacency to work out + const a = try types_store.fresh(); + const b = try types_store.fresh(); + const elem = try types_store.fresh(); + const ext = try types_store.fresh(); + + // Create the tag union content that references type_a_nominal + const a_cons_tag = try env.mkTag("Cons", &[_]Var{ elem, a }); + const a_nil_tag = try env.mkTag("Nil", &[_]Var{}); + const a_backing = try types_store.freshFromContent(try types_store.mkTagUnion(&.{ a_cons_tag, a_nil_tag }, ext)); + try types_store.setVarContent(a, try env.mkNominalType("TypeA", a_backing, &.{})); + + const b_cons_tag = try env.mkTag("Cons", &[_]Var{ elem, b }); + const b_nil_tag = try env.mkTag("Nil", &[_]Var{}); + const b_backing = try types_store.freshFromContent(try types_store.mkTagUnion(&.{ b_cons_tag, b_nil_tag }, ext)); + try types_store.setVarContent(b, try env.mkNominalType("TypeA", b_backing, &.{})); + + const result_nominal_type = try env.unify(a, b); + try std.testing.expectEqual(.ok, result_nominal_type); + + const result_tag_union = try env.unify(a_backing, b_backing); + try std.testing.expectEqual(.ok, result_tag_union); +} + +// static dispatch constraints // + +test "unify - flex with no constraints unifies with flex with constraints" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create constraint: a.sort : List(a) -> List(a) + const list_a = try env.module_env.types.fresh(); + const sort_fn = try env.module_env.types.freshFromContent(try env.mkFuncPure(&[_]Var{list_a}, list_a)); + const sort_constraint = types_mod.StaticDispatchConstraint{ + .fn_name = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("sort")), + .fn_var = sort_fn, + .origin = .where_clause, + }; + + const constraints_range = try env.module_env.types.appendStaticDispatchConstraints(&[_]types_mod.StaticDispatchConstraint{sort_constraint}); + + const a = try env.module_env.types.freshFromContent(.{ .flex = Flex.init() }); + const b = try env.module_env.types.freshFromContent(.{ .flex = .{ + .name = null, + .constraints = constraints_range, + } }); + + const result = try env.unify(a, b); + try std.testing.expectEqual(.ok, result); + + const resolved = env.module_env.types.resolveVar(a); + try std.testing.expect(resolved.desc.content == .flex); + try std.testing.expectEqual(constraints_range, resolved.desc.content.flex.constraints); +} + +test "unify - flex with constraints unifies with flex with same constraints" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + // Create constraint: a.to_str : Str -> Str + const to_str_fn = try env.module_env.types.freshFromContent(try env.mkFuncPure(&[_]Var{str}, str)); + const sort_constraint = types_mod.StaticDispatchConstraint{ + .fn_name = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("to_str")), + .fn_var = to_str_fn, + .origin = .where_clause, + }; + + const a_constraints = try env.module_env.types.appendStaticDispatchConstraints(&[_]types_mod.StaticDispatchConstraint{sort_constraint}); + const b_constraints = try env.module_env.types.appendStaticDispatchConstraints(&[_]types_mod.StaticDispatchConstraint{sort_constraint}); + + const a = try env.module_env.types.freshFromContent(.{ .flex = .{ + .name = null, + .constraints = a_constraints, + } }); + const b = try env.module_env.types.freshFromContent(.{ .flex = .{ + .name = null, + .constraints = b_constraints, + } }); + + const result = try env.unify(a, b); + try std.testing.expectEqual(.ok, result); +} + +test "unify - empty constraints unify with any" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const foo_fn = try env.module_env.types.freshFromContent(try env.mkFuncPure(&[_]Var{str}, str)); + const foo_constraint = types_mod.StaticDispatchConstraint{ + .fn_name = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("foo")), + .fn_var = foo_fn, + .origin = .where_clause, + }; + const constraints = try env.module_env.types.appendStaticDispatchConstraints(&[_]types_mod.StaticDispatchConstraint{foo_constraint}); + + const empty_range = types_mod.StaticDispatchConstraint.SafeList.Range.empty(); + + const a = try env.module_env.types.freshFromContent(.{ .flex = .{ + .name = null, + .constraints = empty_range, + } }); + const b = try env.module_env.types.freshFromContent(.{ .flex = .{ + .name = null, + .constraints = constraints, + } }); + + const result = try env.unify(a, b); + try std.testing.expectEqual(.ok, result); +} + +// capture constraints + +test "unify - flex with constraints vs structure captures deferred check" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + // Create constraint: a.to_str : Str -> Str + const to_str_fn = try env.module_env.types.freshFromContent(try env.mkFuncPure(&[_]Var{str}, str)); + const to_str_constraint = types_mod.StaticDispatchConstraint{ + .fn_name = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("to_str")), + .fn_var = to_str_fn, + .origin = .where_clause, + }; + const constraints = try env.module_env.types.appendStaticDispatchConstraints(&[_]types_mod.StaticDispatchConstraint{to_str_constraint}); + + const flex_var = try env.module_env.types.freshFromContent(.{ .flex = .{ + .name = null, + .constraints = constraints, + } }); + const structure_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const result = try env.unify(flex_var, structure_var); + try std.testing.expectEqual(.ok, result); + + // Check that constraint was captured + try std.testing.expectEqual(1, env.scratch.deferred_constraints.len()); + const deferred = env.scratch.deferred_constraints.items.items[0]; + try std.testing.expectEqual( + env.module_env.types.resolveVar(structure_var).var_, + env.module_env.types.resolveVar(deferred.var_).var_, + ); + try std.testing.expectEqual(constraints, deferred.constraints); +} + +test "unify - structure vs flex with constraints captures deferred check (reversed)" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + // Create constraint: a.to_str : Str -> Str + const to_str_fn = try env.module_env.types.freshFromContent(try env.mkFuncPure(&[_]Var{str}, str)); + const to_str_constraint = types_mod.StaticDispatchConstraint{ + .fn_name = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("to_str")), + .fn_var = to_str_fn, + .origin = .where_clause, + }; + const constraints = try env.module_env.types.appendStaticDispatchConstraints(&[_]types_mod.StaticDispatchConstraint{to_str_constraint}); + + const structure_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + const flex_var = try env.module_env.types.freshFromContent(.{ .flex = .{ + .name = null, + .constraints = constraints, + } }); + + const result = try env.unify(structure_var, flex_var); + try std.testing.expectEqual(.ok, result); + + // Check that constraint was captured (note: vars might be swapped due to merge order) + try std.testing.expectEqual(1, env.scratch.deferred_constraints.len()); + const deferred = env.scratch.deferred_constraints.items.items[0]; + try std.testing.expectEqual( + env.module_env.types.resolveVar(flex_var).var_, + env.module_env.types.resolveVar(deferred.var_).var_, + ); + try std.testing.expectEqual(constraints, deferred.constraints); +} + +test "unify - flex with no constraints vs structure does not capture" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const flex_var = try env.module_env.types.freshFromContent(.{ .flex = Flex.init() }); + const structure_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const result = try env.unify(flex_var, structure_var); + try std.testing.expectEqual(.ok, result); + + // Check that NO constraint was captured + try std.testing.expectEqual(0, env.scratch.deferred_constraints.len()); +} + +test "unify - flex vs nominal type captures constraint" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const str = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + // Create constraint + const ord_fn = try env.module_env.types.freshFromContent(try env.mkFuncPure(&[_]Var{str}, str)); + const ord_constraint = types_mod.StaticDispatchConstraint{ + .fn_name = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("ord")), + .fn_var = ord_fn, + .origin = .where_clause, + }; + const constraints = try env.module_env.types.appendStaticDispatchConstraints(&[_]types_mod.StaticDispatchConstraint{ord_constraint}); + + const flex_var = try env.module_env.types.freshFromContent(.{ .flex = .{ + .name = null, + .constraints = constraints, + } }); + + // Create nominal type (e.g., Path) + const backing_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + const nominal_var = try env.module_env.types.freshFromContent(try env.mkNominalType("Path", backing_var, &[_]Var{})); + + const result = try env.unify(flex_var, nominal_var); + try std.testing.expectEqual(.ok, result); + + // Check that constraint was captured + try std.testing.expectEqual(1, env.scratch.deferred_constraints.len()); + const deferred = env.scratch.deferred_constraints.items.items[0]; + try std.testing.expectEqual( + env.module_env.types.resolveVar(nominal_var).var_, + env.module_env.types.resolveVar(deferred.var_).var_, + ); + try std.testing.expectEqual(constraints, deferred.constraints); +} + +// RecursionVar tests + +test "recursion_var - can be created and points to structure" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create a structure variable (e.g., a Str type) + const structure_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + // Create a RecursionVar pointing to the structure + const rec_var = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = structure_var, + .name = null, + }, + }); + + // Verify the recursion var was created correctly + const resolved = env.module_env.types.resolveVar(rec_var); + try std.testing.expect(resolved.desc.content == .recursion_var); + + const rec_var_content = resolved.desc.content.recursion_var; + try std.testing.expectEqual(structure_var, rec_var_content.structure); +} + +test "recursion_var - unifies with its structure" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create a structure variable + const structure_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + // Create a RecursionVar pointing to the structure + const rec_var = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = structure_var, + .name = null, + }, + }); + + // Unify the recursion var with its structure - this should succeed + // because RecursionVar represents equirecursive types + const result = try env.unify(rec_var, structure_var); + try std.testing.expectEqual(.ok, result); +} + +test "recursion_var - does not cause infinite loop during resolution" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create a circular structure: rec_var -> structure_var -> rec_var + const structure_var = try env.module_env.types.fresh(); + + const rec_var = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = structure_var, + .name = null, + }, + }); + + // Make structure_var point back to rec_var to create a cycle + // This tests that resolveVar and other operations handle cycles properly + _ = try env.unify(structure_var, rec_var); + + // If we get here without hanging, the test passed + // Just verify the vars are connected + const resolved_rec = env.module_env.types.resolveVar(rec_var); + const resolved_structure = env.module_env.types.resolveVar(structure_var); + + // Both should resolve to the same root var + try std.testing.expectEqual(resolved_rec.var_, resolved_structure.var_); +} + +// Equirecursive unification tests + +test "recursion_var - unifies with alias" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create an alias MyStr = Str + const str_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + const alias_content = try env.mkAlias("MyStr", str_var, &[_]Var{}); + const alias_var = try env.module_env.types.freshFromContent(alias_content); + + // Create a RecursionVar pointing to str + const rec_var = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = str_var, + .name = null, + }, + }); + + // RecursionVar should unify with alias by going through to the backing var + const result = try env.unify(rec_var, alias_var); + try std.testing.expectEqual(.ok, result); +} + +test "recursion_var - cannot unify with rigid" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create a rigid var + const rigid_content = try env.mkRigidVar("a"); + const rigid_var = try env.module_env.types.freshFromContent(rigid_content); + + // Create a RecursionVar + const structure_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + const rec_var = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = structure_var, + .name = null, + }, + }); + + // RecursionVar cannot unify with rigid + const result = try env.unify(rec_var, rigid_var); + try std.testing.expectEqual(false, result.isOk()); +} + +test "recursion_var - two recursion vars with same structure unify" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create a shared structure + const structure_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + // Create two RecursionVars both pointing to the same structure + const rec_var_1 = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = structure_var, + .name = null, + }, + }); + + const rec_var_2 = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = structure_var, + .name = null, + }, + }); + + // They should unify successfully + const result = try env.unify(rec_var_1, rec_var_2); + try std.testing.expectEqual(.ok, result); +} + +test "recursion_var - equirecursive unification with nested structure" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create a list structure: List(a) where a is flexible + const elem_var = try env.module_env.types.fresh(); + const list_content = try env.mkList(elem_var); + const list_var = try env.module_env.types.freshFromContent(list_content); + + // Create a RecursionVar that points to the list + const rec_var = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = list_var, + .name = null, + }, + }); + + // Create another list with the same structure + const elem_var_2 = try env.module_env.types.fresh(); + const list_content_2 = try env.mkList(elem_var_2); + const list_var_2 = try env.module_env.types.freshFromContent(list_content_2); + + // RecursionVar should unify with the list + const result = try env.unify(rec_var, list_var_2); + try std.testing.expectEqual(.ok, result); + + // The element vars should also have been unified + const resolved_elem_1 = env.module_env.types.resolveVar(elem_var); + const resolved_elem_2 = env.module_env.types.resolveVar(elem_var_2); + try std.testing.expectEqual(resolved_elem_1.var_, resolved_elem_2.var_); +} + +test "recursion_var - unifies with flex preserving constraints" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create a flex var with static dispatch constraints + const fn_var = try env.module_env.types.freshFromContent(Content{ + .structure = .{ .fn_pure = .{ + .args = VarSafeList.Range.empty(), + .ret = try env.module_env.types.fresh(), + .needs_instantiation = false, + } }, + }); + + const constraint = types_mod.StaticDispatchConstraint{ + .fn_name = try env.module_env.getIdentStore().insert(env.module_env.gpa, Ident.for_text("to_str")), + .fn_var = fn_var, + .origin = .method_call, + }; + + const constraints_range = try env.module_env.types.appendStaticDispatchConstraints(&[_]types_mod.StaticDispatchConstraint{constraint}); + const flex_with_constraints = try env.module_env.types.freshFromContent(Content{ + .flex = Flex.init().withConstraints(constraints_range), + }); + + // Create a RecursionVar + const structure_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + const rec_var = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = structure_var, + .name = null, + }, + }); + + // Unify - should defer the constraints + const result = try env.unify(rec_var, flex_with_constraints); + try std.testing.expectEqual(.ok, result); + + // Should have deferred the constraints + try std.testing.expectEqual(@as(usize, 1), env.scratch.deferred_constraints.len()); +} + +// TypeWriter tests for RecursionVar + +test "type_writer - recursion_var displays structure" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create a structure variable (empty record) + const structure_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + // Create a RecursionVar pointing to it + const rec_var = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = structure_var, + .name = null, + }, + }); + + // Write the recursion var to a string + const TypeWriter = types_mod.TypeWriter; + var writer = try TypeWriter.initFromParts(gpa, &env.module_env.types, env.module_env.getIdentStore(), null); + defer writer.deinit(); + + const result = try writer.writeGet(rec_var, .wrap); + + // Should display as "{}" (the structure it points to) + try std.testing.expectEqualStrings("{}", result); +} + +test "type_writer - recursion_var with cycle displays correctly" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create a self-referential structure: rec_var -> list -> rec_var + const rec_var_placeholder = try env.module_env.types.fresh(); + + // Create a list that points to the placeholder + const list_content = try env.mkList(rec_var_placeholder); + const list_var = try env.module_env.types.freshFromContent(list_content); + + // Create a RecursionVar that points to the list + const rec_var = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = list_var, + .name = null, + }, + }); + + // Unify the placeholder with the rec_var to create the cycle + _ = try env.unify(rec_var_placeholder, rec_var); + + // Write the recursion var + const TypeWriter = types_mod.TypeWriter; + var writer = try TypeWriter.initFromParts(gpa, &env.module_env.types, env.module_env.getIdentStore(), null); + defer writer.deinit(); + + const result = try writer.writeGet(rec_var, .wrap); + + // Should display as "List(...)" - the cycle is detected and shown as "..." + try std.testing.expectEqualStrings("List(...)", result); +} + +test "type_writer - nested recursion_var displays correctly" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Create nested structure: RecursionVar -> List -> RecursionVar -> empty_record + const empty_record_var = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const inner_rec_var = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = empty_record_var, + .name = null, + }, + }); + + const list_content = try env.mkList(inner_rec_var); + const list_var = try env.module_env.types.freshFromContent(list_content); + + const outer_rec_var = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = list_var, + .name = null, + }, + }); + + // Write the outer recursion var + const TypeWriter = types_mod.TypeWriter; + var writer = try TypeWriter.initFromParts(gpa, &env.module_env.types, env.module_env.getIdentStore(), null); + defer writer.deinit(); + + const result = try writer.writeGet(outer_rec_var, .wrap); + + // Should display as "List({})" - following through the RecursionVars + try std.testing.expectEqualStrings("List({})", result); +} + +// Integration test for recursive constraints (motivating example) + +test "recursion_var - integration: deep recursion with RecursionVar prevents infinite loops" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Simulate the motivating example problem: recursive constraint chains + // Without RecursionVar, this would infinite loop during unification + // With RecursionVar, it terminates successfully + + // Create a deep chain of RecursionVars pointing to each other + // var1 -> rec_var1 -> var2 -> rec_var2 -> var3 -> rec_var3 -> var1 (cycle!) + + const var1 = try env.module_env.types.fresh(); + const var2 = try env.module_env.types.fresh(); + const var3 = try env.module_env.types.fresh(); + + // Create rec_var3 pointing to var1 (will create the cycle) + const rec_var3 = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = var1, + .name = null, + }, + }); + + // Create rec_var2 pointing to var3 + const rec_var2 = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = var3, + .name = null, + }, + }); + + // Create rec_var1 pointing to var2 + const rec_var1 = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = var2, + .name = null, + }, + }); + + // Now unify to create the chain + _ = try env.unify(var1, rec_var1); // var1 -> rec_var1 -> var2 + _ = try env.unify(var2, rec_var2); // var2 -> rec_var2 -> var3 + const result = try env.unify(var3, rec_var3); // var3 -> rec_var3 -> var1 (CYCLE!) + + // **KEY TEST**: This should succeed without infinite loop! + // Before RecursionVar, this would hang indefinitely + try std.testing.expectEqual(.ok, result); + + // Verify the unification created connections between the vars + // The exact vars may differ due to redirects, but they should all be connected + const resolved_var1 = env.module_env.types.resolveVar(var1); + const resolved_var2 = env.module_env.types.resolveVar(var2); + const resolved_var3 = env.module_env.types.resolveVar(var3); + + // At least one should be a RecursionVar, indicating the cycle was detected + const has_recursion_var = resolved_var1.desc.content == .recursion_var or + resolved_var2.desc.content == .recursion_var or + resolved_var3.desc.content == .recursion_var; + try std.testing.expect(has_recursion_var); + + // The type should display with cycle detection + const TypeWriter = types_mod.TypeWriter; + var writer = try TypeWriter.initFromParts(gpa, &env.module_env.types, env.module_env.getIdentStore(), null); + defer writer.deinit(); + + const display = try writer.writeGet(var1, .wrap); + + // Should display "..." indicating cycle detection + try std.testing.expect(std.mem.indexOf(u8, display, "...") != null); +} + +test "recursion_var - integration: multiple recursive constraints unify correctly" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Test that two different recursive constraint chains can unify + // Chain A: a1 -> rec_var_a -> a2 -> a1 (cycle) + // Chain B: b1 -> rec_var_b -> b2 -> b1 (cycle) + // They should unify if their base structures match + + // Create chain A + const a1 = try env.module_env.types.fresh(); + const a2 = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const rec_var_a = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = a2, + .name = null, + }, + }); + + _ = try env.unify(a1, rec_var_a); + + // Create chain B with same base structure + const b1 = try env.module_env.types.fresh(); + const b2 = try env.module_env.types.freshFromContent(Content{ .structure = .empty_record }); + + const rec_var_b = try env.module_env.types.freshFromContent(Content{ + .recursion_var = .{ + .structure = b2, + .name = null, + }, + }); + + _ = try env.unify(b1, rec_var_b); + + // Unify chain A with chain B - should succeed because both point to Str + const result = try env.unify(a1, b1); + try std.testing.expectEqual(.ok, result); + + // Verify they unified - both should ultimately point to the same content + // Note: They may not be the exact same var due to redirects, but should be equivalent + const resolved_a1 = env.module_env.types.resolveVar(a1); + const resolved_b1 = env.module_env.types.resolveVar(b1); + + // Check that both resolve to the same structure content + try std.testing.expect(resolved_a1.desc.content == .recursion_var or resolved_a1.desc.content == .structure); + try std.testing.expect(resolved_b1.desc.content == .recursion_var or resolved_b1.desc.content == .structure); + + // The key test: no infinite loop occurred during unification + // If we got here, RecursionVar successfully prevented the infinite loop +} diff --git a/src/check/test/where_clause_test.zig b/src/check/test/where_clause_test.zig new file mode 100644 index 0000000000..e3b3030d13 --- /dev/null +++ b/src/check/test/where_clause_test.zig @@ -0,0 +1,320 @@ +//! Comprehensive tests for where clause type checking functionality. +//! +//! These tests cover: +//! - Basic where clause parsing and type inference +//! - Method constraints on type variables +//! - Multiple constraints +//! - Constraint satisfaction +//! - Error cases + +const std = @import("std"); +const TestEnv = @import("./TestEnv.zig"); + +const testing = std.testing; + +// Basic where clause tests + +test "where clause - basic method constraint infers correctly" { + // Module A defines a type with to_str method + const source_a = + \\A := [Val(Str)].{ + \\ to_str : A -> Str + \\ to_str = |A.Val(s)| s + \\} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + + // Module B uses A and defines a helper with where clause + const source_b = + \\import A + \\ + \\helper : a -> Str where [a.to_str : a -> Str] + \\helper = |x| x.to_str() + \\ + \\main : Str + \\main = helper(A.Val("hello")) + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertDefType( + "helper", + "a -> Str where [a.to_str : a -> Str]", + ); + try test_env_b.assertDefType("main", "Str"); +} + +test "where clause - polymorphic return type" { + const source_a = + \\A := [Val(Str)].{ + \\ to_str : A -> Str + \\ to_str = |A.Val(s)| s + \\} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + + const source_b = + \\import A + \\ + \\helper : a -> b where [a.to_str : a -> b] + \\helper = |x| x.to_str() + \\ + \\main : Str + \\main = helper(A.Val("hello")) + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertDefType( + "helper", + "a -> b where [a.to_str : a -> b]", + ); + try test_env_b.assertDefType("main", "Str"); +} + +test "where clause - constraint with multiple args" { + const source_a = + \\A := [Box(Str)].{ + \\ transform : A, (Str -> Str) -> A + \\ transform = |A.Box(s), fn| A.Box(fn(s)) + \\} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + + const source_b = + \\import A + \\ + \\modify : a, (Str -> Str) -> a where [a.transform : a, (Str -> Str) -> a] + \\modify = |x, fn| x.transform(fn) + \\ + \\main : A + \\main = modify(A.Box("hello"), |s| s) + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertDefType( + "modify", + "a, (Str -> Str) -> a where [a.transform : a, (Str -> Str) -> a]", + ); + try test_env_b.assertDefType("main", "A"); +} + +// Multiple constraints tests + +test "where clause - multiple constraints on same variable" { + const source_a = + \\A := [D(Str, U64)].{ + \\ to_str : A -> Str + \\ to_str = |A.D(s, _)| s + \\ + \\ to_u64 : A -> U64 + \\ to_u64 = |A.D(_, n)| n + \\} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + + const source_b = + \\import A + \\ + \\both : a -> (Str, U64) where [a.to_str : a -> Str, a.to_u64 : a -> U64] + \\both = |x| (x.to_str(), x.to_u64()) + \\ + \\main : (Str, U64) + \\main = both(A.D("hello", 42)) + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertDefType( + "both", + "a -> (Str, U64) where [a.to_str : a -> Str, a.to_u64 : a -> U64]", + ); + try test_env_b.assertDefType("main", "(Str, U64)"); +} + +// Cross-module where clause tests + +test "where clause - cross-module constraint satisfaction" { + const source_a = + \\A := [A(Str)].{ + \\ to_str : A -> Str + \\ to_str = |A.A(val)| val + \\} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + try test_env_a.assertDefType("A.to_str", "A -> Str"); + + const source_b = + \\import A + \\ + \\helper : a -> Str where [a.to_str : a -> Str] + \\helper = |x| x.to_str() + \\ + \\main : Str + \\main = helper(A.A("hello")) + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertDefType( + "helper", + "a -> Str where [a.to_str : a -> Str]", + ); + try test_env_b.assertDefType("main", "Str"); +} + +test "where clause - cross-module polymorphic constraint" { + const source_a = + \\A := [A(Str)].{ + \\ to_str = |A.A(val)| val + \\ to_str2 = |x| x.to_str() + \\} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + try test_env_a.assertDefType("A.to_str", "A -> Str"); + try test_env_a.assertDefType( + "A.to_str2", + "a -> b where [a.to_str : a -> b]", + ); + + const source_b = + \\import A + \\ + \\main : Str + \\main = (A.A("hello")).to_str2() + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertDefType("main", "Str"); +} + +// Nested/chained where clause tests + +test "where clause - chained method calls" { + const source_a = + \\A := [Box(Str)].{ + \\ get_value : A -> Str + \\ get_value = |A.Box(s)| s + \\ + \\ transform : A, (Str -> Str) -> A + \\ transform = |A.Box(s), fn| A.Box(fn(s)) + \\} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + + const source_b = + \\import A + \\ + \\chain : a, (Str -> Str) -> Str where [a.transform : a, (Str -> Str) -> a, a.get_value : a -> Str] + \\chain = |x, fn| x.transform(fn).get_value() + \\ + \\main : Str + \\main = chain(A.Box("hello"), |s| s) + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertDefType("main", "Str"); +} + +// Error case tests + +test "where clause - missing method on type" { + const source_a = + \\A := [Val(Str)].{} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + + const source_b = + \\import A + \\ + \\helper : a -> Str where [a.to_str : a -> Str] + \\helper = |x| x.to_str() + \\ + \\main = helper(A.Val("hello")) + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertFirstTypeError("MISSING METHOD"); +} + +test "where clause - method signature mismatch" { + const source_a = + \\A := [Val(Str)].{ + \\ to_str : A -> U64 + \\ to_str = |_| 42 + \\} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + + const source_b = + \\import A + \\ + \\helper : a -> Str where [a.to_str : a -> Str] + \\helper = |x| x.to_str() + \\ + \\main = helper(A.Val("hello")) + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertFirstTypeError("TYPE MISMATCH"); +} + +// Let polymorphism with where clauses + +test "where clause - same type used multiple times with where constraint" { + const source_a = + \\A := [A(Str)].{ + \\ to_str : A -> Str + \\ to_str = |A.A(s)| s + \\} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + + const source_b = + \\import A + \\ + \\helper : a -> Str where [a.to_str : a -> Str] + \\helper = |x| x.to_str() + \\ + \\main : (Str, Str) + \\main = (helper(A.A("hello")), helper(A.A("world"))) + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertDefType("main", "(Str, Str)"); +} + +// Where clause without annotation (inferred) + +test "where clause - inferred from method call without annotation" { + const source_a = + \\A := [Val(Str)].{ + \\ to_str : A -> Str + \\ to_str = |A.Val(s)| s + \\} + ; + var test_env_a = try TestEnv.init("A", source_a); + defer test_env_a.deinit(); + + const source_b = + \\import A + \\ + \\helper = |x| x.to_str() + \\ + \\main : Str + \\main = helper(A.Val("hello")) + ; + var test_env_b = try TestEnv.initWithImport("B", source_b, "A", &test_env_a); + defer test_env_b.deinit(); + try test_env_b.assertDefType( + "helper", + "a -> b where [a.to_str : a -> b]", + ); + try test_env_b.assertDefType("main", "Str"); +} diff --git a/src/check/unify.zig b/src/check/unify.zig index 5d5048dc53..10017ddba3 100644 --- a/src/check/unify.zig +++ b/src/check/unify.zig @@ -1,4 +1,5 @@ //! This module implements Hindley-Milner style type unification with extensions for: +//! //! * flex/rigid variables //! * type aliases //! * tuples @@ -40,11 +41,13 @@ //! subsequent unification runs. const std = @import("std"); +const builtin = @import("builtin"); const base = @import("base"); const tracy = @import("tracy"); const collections = @import("collections"); const types_mod = @import("types"); const can = @import("can"); +const copy_import = @import("copy_import.zig"); const Check = @import("check").Check; const problem_mod = @import("problem.zig"); @@ -52,9 +55,12 @@ const occurs = @import("occurs.zig"); const snapshot_mod = @import("snapshot.zig"); const ModuleEnv = can.ModuleEnv; +const AutoImportedType = can.Can.AutoImportedType; +const CIR = can.CIR; const Region = base.Region; const Ident = base.Ident; +const MkSafeList = collections.SafeList; const SmallStringInterner = collections.SmallStringInterner; @@ -67,6 +73,9 @@ const Var = types_mod.Var; const Desc = types_mod.Descriptor; const Rank = types_mod.Rank; const Mark = types_mod.Mark; +const Flex = types_mod.Flex; +const Rigid = types_mod.Rigid; +const RecursionVar = types_mod.RecursionVar; const Content = types_mod.Content; const Alias = types_mod.Alias; const NominalType = types_mod.NominalType; @@ -74,7 +83,6 @@ const FlatType = types_mod.FlatType; const Builtin = types_mod.Builtin; const Tuple = types_mod.Tuple; const Num = types_mod.Num; -const NumCompact = types_mod.Num.Compact; const Func = types_mod.Func; const Record = types_mod.Record; const RecordField = types_mod.RecordField; @@ -82,6 +90,8 @@ const TwoRecordFields = types_mod.TwoRecordFields; const TagUnion = types_mod.TagUnion; const Tag = types_mod.Tag; const TwoTags = types_mod.TwoTags; +const StaticDispatchConstraint = types_mod.StaticDispatchConstraint; +const TwoStaticDispatchConstraints = types_mod.TwoStaticDispatchConstraints; const VarSafeList = Var.SafeList; const RecordFieldSafeMultiList = RecordField.SafeMultiList; @@ -114,44 +124,69 @@ pub const Result = union(enum) { } }; -/// Unify two type variables with context about whether this is from an annotation -pub fn unifyWithContext( +/// Unify two type variables +/// +/// This function +/// * Resolves type variables & compresses paths +/// * Compares variable contents for equality +/// * Merges unified variables so 1 is "root" and the other is "redirect" +pub fn unify( module_env: *ModuleEnv, types: *types_mod.Store, problems: *problem_mod.Store, snapshots: *snapshot_mod.Store, + type_writer: *types_mod.TypeWriter, unify_scratch: *Scratch, occurs_scratch: *occurs.Scratch, + /// The "expected" variable a: Var, + /// The "actual" variable b: Var, - from_annotation: bool, ) std.mem.Allocator.Error!Result { - return unifyWithConstraintOrigin( + return unifyWithConf( module_env, types, problems, snapshots, + type_writer, unify_scratch, occurs_scratch, a, b, - from_annotation, - null, + Conf{ .ctx = .anon, .constraint_origin_var = null }, ); } -/// Unify two types, tracking the origin of the constraint for better error reporting -pub fn unifyWithConstraintOrigin( +/// Conf about a unification, used to improve error messages +pub const Conf = struct { + ctx: Ctx, + constraint_origin_var: ?Var, + + /// If the "expect" var comes fro an annotation, or if it's anonymous + pub const Ctx = enum { anon, anno }; +}; + +/// Unify two type variables +/// +/// This function +/// * Resolves type variables & compresses paths +/// * Compares variable contents for equality +/// * Merges unified variables so 1 is "root" and the other is "redirect" +/// +/// This function accepts a context and optional constraint origin var (for better error reporting) +pub fn unifyWithConf( module_env: *ModuleEnv, types: *types_mod.Store, problems: *problem_mod.Store, snapshots: *snapshot_mod.Store, + type_writer: *types_mod.TypeWriter, unify_scratch: *Scratch, occurs_scratch: *occurs.Scratch, + /// The "expected" variable a: Var, + /// The "actual" variable b: Var, - from_annotation: bool, - constraint_origin_var: ?Var, + conf: Conf, ) std.mem.Allocator.Error!Result { const trace = tracy.trace(@src()); defer trace.end(); @@ -160,7 +195,7 @@ pub fn unifyWithConstraintOrigin( unify_scratch.reset(); // Unify - var unifier = Unifier(*types_mod.Store).init(module_env, types, unify_scratch, occurs_scratch); + var unifier = Unifier.init(module_env, types, unify_scratch, occurs_scratch); unifier.unifyGuarded(a, b) catch |err| { const problem: Problem = blk: { switch (err) { @@ -168,8 +203,8 @@ pub fn unifyWithConstraintOrigin( return error.OutOfMemory; }, error.TypeMismatch => { - const expected_snapshot = try snapshots.deepCopyVar(types, a); - const actual_snapshot = try snapshots.deepCopyVar(types, b); + const expected_snapshot = try snapshots.snapshotVarForError(types, type_writer, a); + const actual_snapshot = try snapshots.snapshotVarForError(types, type_writer, b); break :blk .{ .type_mismatch = .{ .types = .{ @@ -177,8 +212,8 @@ pub fn unifyWithConstraintOrigin( .expected_snapshot = expected_snapshot, .actual_var = b, .actual_snapshot = actual_snapshot, - .from_annotation = from_annotation, - .constraint_origin_var = constraint_origin_var, + .from_annotation = conf.ctx == .anno, + .constraint_origin_var = conf.constraint_origin_var, }, .detail = null, } }; @@ -191,11 +226,6 @@ pub fn unifyWithConstraintOrigin( // Check if 'a' is the literal (has int_poly/num_poly/unbound types) or 'b' is const literal_is_a = switch (a_resolved.desc.content) { .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .int_poly, .num_poly, .int_unbound, .num_unbound, .frac_unbound => true, - else => false, - }, - .list_unbound => true, .record_unbound => true, else => false, }, @@ -204,7 +234,7 @@ pub fn unifyWithConstraintOrigin( const literal_var = if (literal_is_a) a else b; const expected_var = if (literal_is_a) b else a; - const expected_snapshot = try snapshots.deepCopyVar(types, expected_var); + const expected_snapshot = try snapshots.snapshotVarForError(types, type_writer, expected_var); break :blk .{ .number_does_not_fit = .{ .literal_var = literal_var, @@ -219,11 +249,6 @@ pub fn unifyWithConstraintOrigin( // Check if 'a' is the literal (has int_poly/num_poly/unbound types) or 'b' is const literal_is_a = switch (a_resolved.desc.content) { .structure => |structure| switch (structure) { - .num => |num| switch (num) { - .int_poly, .num_poly, .int_unbound, .num_unbound, .frac_unbound => true, - else => false, - }, - .list_unbound => true, .record_unbound => true, else => false, }, @@ -232,7 +257,7 @@ pub fn unifyWithConstraintOrigin( const literal_var = if (literal_is_a) a else b; const expected_var = if (literal_is_a) b else a; - const expected_snapshot = try snapshots.deepCopyVar(types, expected_var); + const expected_snapshot = try snapshots.snapshotVarForError(types, type_writer, expected_var); break :blk .{ .negative_unsigned_int = .{ .literal_var = literal_var, @@ -255,35 +280,35 @@ pub fn unifyWithConstraintOrigin( if (unify_scratch.err) |unify_err| { switch (unify_err) { .recursion_anonymous => |var_| { - // TODO: Snapshot infinite recursion - // const snapshot = snapshots.deepCopyVar(types, var_); + const snapshot = try snapshots.snapshotVarForError(types, type_writer, var_); break :blk .{ .anonymous_recursion = .{ .var_ = var_, + .snapshot = snapshot, } }; }, .recursion_infinite => |var_| { - // TODO: Snapshot infinite recursion - // const snapshot = snapshots.deepCopyVar(types, var_); + const snapshot = try snapshots.snapshotVarForError(types, type_writer, var_); break :blk .{ .infinite_recursion = .{ .var_ = var_, + .snapshot = snapshot, } }; }, .invalid_number_type => |var_| { - const snapshot = try snapshots.deepCopyVar(types, var_); + const snapshot = try snapshots.snapshotVarForError(types, type_writer, var_); break :blk .{ .invalid_number_type = .{ .var_ = var_, .snapshot = snapshot, } }; }, .invalid_record_ext => |var_| { - const snapshot = try snapshots.deepCopyVar(types, var_); + const snapshot = try snapshots.snapshotVarForError(types, type_writer, var_); break :blk .{ .invalid_record_ext = .{ .var_ = var_, .snapshot = snapshot, } }; }, .invalid_tag_union_ext => |var_| { - const snapshot = try snapshots.deepCopyVar(types, var_); + const snapshot = try snapshots.snapshotVarForError(types, type_writer, var_); break :blk .{ .invalid_tag_union_ext = .{ .var_ = var_, .snapshot = snapshot, @@ -291,11 +316,13 @@ pub fn unifyWithConstraintOrigin( }, } } else { + const expected_snapshot = try snapshots.snapshotVarForError(types, type_writer, a); + const actual_snapshot = try snapshots.snapshotVarForError(types, type_writer, b); break :blk .{ .bug = .{ .expected_var = a, - .expected = try snapshots.deepCopyVar(types, a), + .expected = expected_snapshot, .actual_var = b, - .actual = try snapshots.deepCopyVar(types, b), + .actual = actual_snapshot, } }; } }, @@ -313,36 +340,6 @@ pub fn unifyWithConstraintOrigin( return .ok; } -/// Unify two type variables -/// -/// This function -/// * Resolves type variables & compresses paths -/// * Compares variable contents for equality -/// * Merges unified variables so 1 is "root" and the other is "redirect" -pub fn unify( - module_env: *ModuleEnv, - types: *types_mod.Store, - problems: *problem_mod.Store, - snapshots: *snapshot_mod.Store, - unify_scratch: *Scratch, - occurs_scratch: *occurs.Scratch, - a: Var, - b: Var, -) std.mem.Allocator.Error!Result { - // Default to not from annotation for backward compatibility - return unifyWithContext( - module_env, - types, - problems, - snapshots, - unify_scratch, - occurs_scratch, - a, - b, - false, // from_annotation = false by default - ); -} - /// A temporary unification context used to unify two type variables within a `Store`. /// /// `Unifier` is created per unification call and: @@ -361,2307 +358,1969 @@ pub fn unify( /// * basic support for function, tuple, and number types /// /// Callers are not expected to construct `Unifier`. Instead call `unify(...)`. -fn Unifier(comptime StoreTypeB: type) type { - return struct { - const Self = @This(); +const Unifier = struct { + const Self = @This(); + module_env: *ModuleEnv, + types_store: *types_mod.Store, + scratch: *Scratch, + occurs_scratch: *occurs.Scratch, + depth: u8, + skip_depth_check: bool, + + /// Init unifier + pub fn init( module_env: *ModuleEnv, - types_store: StoreTypeB, + types_store: *types_mod.Store, scratch: *Scratch, occurs_scratch: *occurs.Scratch, - depth: u8, - skip_depth_check: bool, - - /// Init unifier - pub fn init( - module_env: *ModuleEnv, - types_store: *types_mod.Store, - scratch: *Scratch, - occurs_scratch: *occurs.Scratch, - ) Unifier(*types_mod.Store) { - return .{ - .module_env = module_env, - .types_store = types_store, - .scratch = scratch, - .occurs_scratch = occurs_scratch, - .depth = 0, - .skip_depth_check = false, - }; - } - - // merge - - /// Link the variables & updated the content in the type_store - /// In the old compiler, this function was called "merge" - fn merge(self: *Self, vars: *const ResolvedVarDescs, new_content: Content) void { - self.types_store.union_(vars.a.var_, vars.b.var_, .{ - .content = new_content, - .rank = Rank.min(vars.a.desc.rank, vars.b.desc.rank), - .mark = Mark.none, - }); - } - - /// Create a new type variable *in this pool* - fn fresh(self: *Self, vars: *const ResolvedVarDescs, new_content: Content) std.mem.Allocator.Error!Var { - const var_ = try self.types_store.register(.{ - .content = new_content, - .rank = Rank.min(vars.a.desc.rank, vars.b.desc.rank), - .mark = Mark.none, - }); - _ = try self.scratch.fresh_vars.append(self.scratch.gpa, var_); - return var_; - } - - // unification - - const Error = error{ - TypeMismatch, - UnifyErr, - NumberDoesNotFit, - NegativeUnsignedInt, - AllocatorError, + ) Unifier { + return .{ + .module_env = module_env, + .types_store = types_store, + .scratch = scratch, + .occurs_scratch = occurs_scratch, + .depth = 0, + .skip_depth_check = false, }; - - const max_depth_before_occurs = 8; - - fn unifyGuarded(self: *Self, a_var: Var, b_var: Var) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - switch (self.types_store.checkVarsEquiv(a_var, b_var)) { - .equiv => { - // this means that the vars point to the same exact type - // descriptor, so nothing needs to happen - return; - }, - .not_equiv => |vars| { - if (self.skip_depth_check or self.depth < max_depth_before_occurs) { - self.depth += 1; - const result = self.unifyVars(&vars); - self.depth -= 1; - _ = try result; - } else { - try self.checkRecursive(&vars); - - self.skip_depth_check = true; - try self.unifyVars(&vars); - self.skip_depth_check = false; - } - }, - } - } - - /// Unify two vars - /// Internal entry point for unification logic. Use `unifyGuarded` to ensure - /// proper depth tracking and occurs checking. - fn unifyVars(self: *Self, vars: *const ResolvedVarDescs) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - switch (vars.a.desc.content) { - .flex_var => |mb_a_ident| { - self.unifyFlex(vars, mb_a_ident, vars.b.desc.content); - }, - .rigid_var => |_| { - try self.unifyRigid(vars, vars.b.desc.content); - }, - .alias => |a_alias| { - const backing_var = self.types_store.getAliasBackingVar(a_alias); - const backing_resolved = self.types_store.resolveVar(backing_var); - if (backing_resolved.desc.content == .err) { - // Invalid alias - treat as transparent - self.merge(vars, vars.b.desc.content); - return; - } - try self.unifyAlias(vars, a_alias, vars.b.desc.content); - }, - .structure => |a_flat_type| { - try self.unifyStructure(vars, a_flat_type, vars.b.desc.content); - }, - .err => self.merge(vars, .err), - } - } - - /// Run a full occurs check on each variable, erroring if it is infinite - /// or anonymous recursion - /// - /// This function is called when unify has recursed a sufficient depth that - /// a recursive type seems likely. - fn checkRecursive(self: *Self, vars: *const ResolvedVarDescs) Error!void { - const a_occurs = occurs.occurs(self.types_store, self.occurs_scratch, vars.a.var_) catch return Error.AllocatorError; - switch (a_occurs) { - .not_recursive => {}, - .recursive_nominal => {}, - .recursive_anonymous => { - return self.setUnifyErrAndThrow(UnifyErrCtx{ .recursion_anonymous = vars.a.var_ }); - }, - .infinite => { - return self.setUnifyErrAndThrow(UnifyErrCtx{ .recursion_infinite = vars.a.var_ }); - }, - } - - const b_occurs = occurs.occurs(self.types_store, self.occurs_scratch, vars.b.var_) catch return Error.AllocatorError; - switch (b_occurs) { - .not_recursive => {}, - .recursive_nominal => {}, - .recursive_anonymous => { - return self.setUnifyErrAndThrow(UnifyErrCtx{ .recursion_anonymous = vars.b.var_ }); - }, - .infinite => { - return self.setUnifyErrAndThrow(UnifyErrCtx{ .recursion_infinite = vars.b.var_ }); - }, - } - } - - // Unify flex // - - /// Unify when `a` was a flex - fn unifyFlex(self: *Self, vars: *const ResolvedVarDescs, mb_a_ident: ?Ident.Idx, b_content: Content) void { - const trace = tracy.trace(@src()); - defer trace.end(); - - switch (b_content) { - .flex_var => |mb_b_ident| { - if (mb_a_ident) |a_ident| { - self.merge(vars, Content{ .flex_var = a_ident }); - } else { - self.merge(vars, Content{ .flex_var = mb_b_ident }); - } - }, - .rigid_var => self.merge(vars, b_content), - .alias => |_| self.merge(vars, b_content), - .structure => self.merge(vars, b_content), - .err => self.merge(vars, .err), - } - } - - // Unify rigid // - - /// Unify when `a` was a rigid - fn unifyRigid(self: *Self, vars: *const ResolvedVarDescs, b_content: Content) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - switch (b_content) { - .flex_var => self.merge(vars, vars.a.desc.content), - .rigid_var => return error.TypeMismatch, - .alias => return error.TypeMismatch, - .structure => return error.TypeMismatch, - .err => self.merge(vars, .err), - } - } - - // Unify alias // - - /// Unify when `a` was a alias - fn unifyAlias(self: *Self, vars: *const ResolvedVarDescs, a_alias: Alias, b_content: Content) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - const backing_var = self.types_store.getAliasBackingVar(a_alias); - - switch (b_content) { - .flex_var => |_| { - self.merge(vars, Content{ .alias = a_alias }); - }, - .rigid_var => |_| { - try self.unifyGuarded(backing_var, vars.b.var_); - }, - .alias => |b_alias| { - const b_backing_var = self.types_store.getAliasBackingVar(b_alias); - // TODO: Do we need this? - // const b_backing_resolved = self.types_store.resolveVar(b_backing_var); - // if (b_backing_resolved.desc.content == .err) { - // // Invalid alias - treat as transparent - // self.merge(vars, vars.a.desc.content); - // return; - // } - if (TypeIdent.eql(self.module_env.getIdentStore(), a_alias.ident, b_alias.ident)) { - try self.unifyTwoAliases(vars, a_alias, b_alias); - } else { - try self.unifyGuarded(backing_var, b_backing_var); - } - }, - .structure => { - try self.unifyGuarded(backing_var, vars.b.var_); - }, - .err => self.merge(vars, .err), - } - } - - /// Unify two aliases - /// - /// This function assumes the caller has already checked that the alias names match - /// - /// this checks: - /// * that the arities are the same - /// * that parallel arguments unify - /// - /// NOTE: the rust version of this function `unify_two_aliases` is *significantly* more - /// complicated than the version here - fn unifyTwoAliases(self: *Self, vars: *const ResolvedVarDescs, a_alias: Alias, b_alias: Alias) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - if (a_alias.vars.nonempty.count != b_alias.vars.nonempty.count) { - return error.TypeMismatch; - } - - // Unify each pair of arguments - const a_args_slice = self.types_store.sliceAliasArgs(a_alias); - const b_args_slice = self.types_store.sliceAliasArgs(b_alias); - for (a_args_slice, b_args_slice) |a_arg, b_arg| { - try self.unifyGuarded(a_arg, b_arg); - } - - // Rust compiler comment: - // Don't report real_var mismatches, because they must always be surfaced higher, from the argument types. - const a_backing_var = self.types_store.getAliasBackingVar(a_alias); - const b_backing_var = self.types_store.getAliasBackingVar(b_alias); - self.unifyGuarded(a_backing_var, b_backing_var) catch {}; - - // Ensure the target variable has slots for the alias arguments - self.merge(vars, vars.b.desc.content); - } - - // Unify structure // - - /// Unify when `a` is a structure type - fn unifyStructure( - self: *Self, - vars: *const ResolvedVarDescs, - a_flat_type: FlatType, - b_content: Content, - ) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - switch (b_content) { - .flex_var => |_| { - self.merge(vars, Content{ .structure = a_flat_type }); - }, - .rigid_var => return error.TypeMismatch, - .alias => |b_alias| { - try self.unifyGuarded(vars.a.var_, self.types_store.getAliasBackingVar(b_alias)); - }, - .structure => |b_flat_type| { - try self.unifyFlatType(vars, a_flat_type, b_flat_type); - }, - .err => self.merge(vars, .err), - } - } - - /// Unify when `a` is a structure type - fn unifyFlatType( - self: *Self, - vars: *const ResolvedVarDescs, - a_flat_type: FlatType, - b_flat_type: FlatType, - ) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - switch (a_flat_type) { - .str => { - switch (b_flat_type) { - .str => self.merge(vars, vars.b.desc.content), - .nominal_type => |b_type| { - const b_backing_var = self.types_store.getNominalBackingVar(b_type); - const b_backing_resolved = self.types_store.resolveVar(b_backing_var); - if (b_backing_resolved.desc.content == .err) { - // Invalid nominal type - treat as transparent - self.merge(vars, vars.a.desc.content); - return; - } - return error.TypeMismatch; - }, - else => return error.TypeMismatch, - } - }, - .box => |a_var| { - switch (b_flat_type) { - .box => |b_var| { - try self.unifyGuarded(a_var, b_var); - self.merge(vars, vars.b.desc.content); - }, - else => return error.TypeMismatch, - } - }, - .list => |a_var| { - switch (b_flat_type) { - .list => |b_var| { - try self.unifyGuarded(a_var, b_var); - self.merge(vars, vars.b.desc.content); - }, - .list_unbound => { - // When unifying list with list_unbound, list wins - self.merge(vars, vars.a.desc.content); - }, - else => return error.TypeMismatch, - } - }, - .list_unbound => { - switch (b_flat_type) { - .list => |_| { - // When unifying list_unbound with list, list wins - self.merge(vars, vars.b.desc.content); - }, - .list_unbound => { - // Both are list_unbound - stay unbound - self.merge(vars, vars.a.desc.content); - }, - else => return error.TypeMismatch, - } - }, - .tuple => |a_tuple| { - switch (b_flat_type) { - .tuple => |b_tuple| { - try self.unifyTuple(vars, a_tuple, b_tuple); - }, - else => return error.TypeMismatch, - } - }, - .num => |a_num| { - switch (b_flat_type) { - .num => |b_num| { - try self.unifyNum(vars, a_num, b_num); - }, - else => return error.TypeMismatch, - } - }, - .nominal_type => |a_type| { - const a_backing_var = self.types_store.getNominalBackingVar(a_type); - const a_backing_resolved = self.types_store.resolveVar(a_backing_var); - if (a_backing_resolved.desc.content == .err) { - // Invalid nominal type - treat as transparent - self.merge(vars, vars.b.desc.content); - return; - } - - switch (b_flat_type) { - .nominal_type => |b_type| { - const b_backing_var = self.types_store.getNominalBackingVar(b_type); - const b_backing_resolved = self.types_store.resolveVar(b_backing_var); - if (b_backing_resolved.desc.content == .err) { - // Invalid nominal type - treat as transparent - self.merge(vars, vars.a.desc.content); - return; - } - - try self.unifyNominalType(vars, a_type, b_type); - }, - else => return error.TypeMismatch, - } - }, - .fn_pure => |a_func| { - switch (b_flat_type) { - .fn_pure => |b_func| { - try self.unifyFunc(vars, a_func, b_func); - self.merge(vars, vars.a.desc.content); - }, - .fn_unbound => |b_func| { - // pure unifies with unbound -> pure - try self.unifyFunc(vars, a_func, b_func); - self.merge(vars, vars.a.desc.content); - }, - .fn_effectful => { - // pure cannot unify with effectful - return error.TypeMismatch; - }, - else => return error.TypeMismatch, - } - }, - .fn_effectful => |a_func| { - switch (b_flat_type) { - .fn_effectful => |b_func| { - try self.unifyFunc(vars, a_func, b_func); - self.merge(vars, vars.a.desc.content); - }, - .fn_unbound => |b_func| { - // effectful unifies with unbound -> effectful - try self.unifyFunc(vars, a_func, b_func); - self.merge(vars, vars.a.desc.content); - }, - .fn_pure => { - // effectful cannot unify with pure - return error.TypeMismatch; - }, - else => return error.TypeMismatch, - } - }, - .fn_unbound => |a_func| { - switch (b_flat_type) { - .fn_pure => |b_func| { - // unbound unifies with pure -> pure - try self.unifyFunc(vars, a_func, b_func); - self.merge(vars, vars.b.desc.content); - }, - .fn_effectful => |b_func| { - // unbound unifies with effectful -> effectful - try self.unifyFunc(vars, a_func, b_func); - self.merge(vars, vars.b.desc.content); - }, - .fn_unbound => |b_func| { - // unbound unifies with unbound -> unbound - try self.unifyFunc(vars, a_func, b_func); - self.merge(vars, vars.a.desc.content); - }, - else => return error.TypeMismatch, - } - }, - .record => |a_record| { - switch (b_flat_type) { - .empty_record => { - if (a_record.fields.len() == 0) { - try self.unifyGuarded(a_record.ext, vars.b.var_); - } else { - return error.TypeMismatch; - } - }, - .record => |b_record| { - try self.unifyTwoRecords(vars, a_record, b_record); - }, - .record_unbound => |b_fields| { - // When unifying record with record_unbound, record wins - // First gather the fields from the record - const a_gathered_fields = try self.gatherRecordFields(a_record); - - // For record_unbound, we just have the fields directly (no extension) - const b_gathered_range = self.scratch.copyGatherFieldsFromMultiList( - &self.types_store.record_fields, - b_fields, - ) catch return Error.AllocatorError; - - // Partition the fields - const partitioned = Self.partitionFields( - self.module_env.getIdentStore(), - self.scratch, - a_gathered_fields.range, - b_gathered_range, - ) catch return Error.AllocatorError; - - // record_unbound requires at least its fields to be present in the record - // The record can have additional fields (that's what makes it extensible) - if (partitioned.only_in_b.len() > 0) { - // The record_unbound has fields that the record doesn't have - return error.TypeMismatch; - } - - // Unify shared fields - try self.unifySharedFields( - vars, - self.scratch.in_both_fields.sliceRange(partitioned.in_both), - null, - null, - a_gathered_fields.ext, - ); - - // Record wins (keeps its extension and any extra fields) - self.merge(vars, vars.a.desc.content); - }, - .record_poly => |b_poly| { - // When unifying record with record_poly, unify the records - try self.unifyTwoRecords(vars, a_record, b_poly.record); - }, - else => return error.TypeMismatch, - } - }, - .record_unbound => |a_fields| { - switch (b_flat_type) { - .empty_record => { - if (a_fields.len() == 0) { - // Both are empty, merge as empty_record - self.merge(vars, Content{ .structure = .empty_record }); - } else { - return error.TypeMismatch; - } - }, - .record => |b_record| { - // When unifying record_unbound with record, record wins - // Copy unbound fields into scratch - const a_gathered_range = self.scratch.copyGatherFieldsFromMultiList( - &self.types_store.record_fields, - a_fields, - ) catch return Error.AllocatorError; - - // Gather fields from the record - const b_gathered_fields = try self.gatherRecordFields(b_record); - - // Partition the fields - const partitioned = Self.partitionFields( - self.module_env.getIdentStore(), - self.scratch, - a_gathered_range, - b_gathered_fields.range, - ) catch return Error.AllocatorError; - - // record_unbound requires at least its fields to be present in the record - // The record can have additional fields (that's what makes it extensible) - if (partitioned.only_in_a.len() > 0) { - // The record_unbound has fields that the record doesn't have - return error.TypeMismatch; - } - - // Unify shared fields - try self.unifySharedFields( - vars, - self.scratch.in_both_fields.sliceRange(partitioned.in_both), - null, - null, - b_gathered_fields.ext, - ); - - // Record wins - self.merge(vars, vars.b.desc.content); - }, - .record_unbound => |b_fields| { - // Both are record_unbound - unify fields and stay unbound - // Copy both field sets into scratch - const a_gathered_range = self.scratch.copyGatherFieldsFromMultiList( - &self.types_store.record_fields, - a_fields, - ) catch return Error.AllocatorError; - const b_gathered_range = self.scratch.copyGatherFieldsFromMultiList( - &self.types_store.record_fields, - b_fields, - ) catch return Error.AllocatorError; - - // Partition the fields - const partitioned = Self.partitionFields( - self.module_env.getIdentStore(), - self.scratch, - a_gathered_range, - b_gathered_range, - ) catch return Error.AllocatorError; - - // Check that they have the same fields - if (partitioned.only_in_a.len() > 0 or partitioned.only_in_b.len() > 0) { - return error.TypeMismatch; - } - - // Unify shared fields (no extension since both are unbound) - const dummy_ext = self.fresh(vars, .{ .structure = .empty_record }) catch return Error.AllocatorError; - try self.unifySharedFields( - vars, - self.scratch.in_both_fields.sliceRange(partitioned.in_both), - null, - null, - dummy_ext, - ); - - // Stay unbound (use the first one's fields since they're unified now) - self.merge(vars, vars.a.desc.content); - }, - .record_poly => |b_poly| { - // When unifying record_unbound with record_poly, poly wins - // Copy unbound fields into scratch - const a_gathered_range = self.scratch.copyGatherFieldsFromMultiList( - &self.types_store.record_fields, - a_fields, - ) catch return Error.AllocatorError; - - // Gather fields from the poly record - const b_gathered_fields = try self.gatherRecordFields(b_poly.record); - - // Partition the fields - const partitioned = Self.partitionFields( - self.module_env.getIdentStore(), - self.scratch, - a_gathered_range, - b_gathered_fields.range, - ) catch return Error.AllocatorError; - - // Check that they have the same fields - if (partitioned.only_in_a.len() > 0 or partitioned.only_in_b.len() > 0) { - return error.TypeMismatch; - } - - // Unify shared fields - try self.unifySharedFields( - vars, - self.scratch.in_both_fields.sliceRange(partitioned.in_both), - null, - null, - b_gathered_fields.ext, - ); - - // Poly wins - self.merge(vars, vars.b.desc.content); - }, - else => return error.TypeMismatch, - } - }, - .record_poly => |a_poly| { - switch (b_flat_type) { - .empty_record => { - if (a_poly.record.fields.len() == 0) { - try self.unifyGuarded(a_poly.record.ext, vars.b.var_); - } else { - return error.TypeMismatch; - } - }, - .record => |b_record| { - // When unifying record_poly with record, unify the records - try self.unifyTwoRecords(vars, a_poly.record, b_record); - }, - .record_unbound => |b_fields| { - // When unifying record_poly with record_unbound, poly wins - // Gather fields from the poly record - const a_gathered_fields = try self.gatherRecordFields(a_poly.record); - - // Copy unbound fields into scratch - const b_gathered_range = self.scratch.copyGatherFieldsFromMultiList( - &self.types_store.record_fields, - b_fields, - ) catch return Error.AllocatorError; - - // Partition the fields - const partitioned = Self.partitionFields( - self.module_env.getIdentStore(), - self.scratch, - a_gathered_fields.range, - b_gathered_range, - ) catch return Error.AllocatorError; - - // Check that they have the same fields - if (partitioned.only_in_a.len() > 0 or partitioned.only_in_b.len() > 0) { - return error.TypeMismatch; - } - - // Unify shared fields - try self.unifySharedFields( - vars, - self.scratch.in_both_fields.sliceRange(partitioned.in_both), - null, - null, - a_gathered_fields.ext, - ); - - // Poly wins - self.merge(vars, vars.a.desc.content); - }, - .record_poly => |b_poly| { - // Both are record_poly - unify the records and vars - try self.unifyTwoRecords(vars, a_poly.record, b_poly.record); - try self.unifyGuarded(a_poly.var_, b_poly.var_); - }, - else => return error.TypeMismatch, - } - }, - .empty_record => { - switch (b_flat_type) { - .empty_record => { - self.merge(vars, Content{ .structure = .empty_record }); - }, - .record => |b_record| { - if (b_record.fields.len() == 0) { - try self.unifyGuarded(vars.a.var_, b_record.ext); - } else { - return error.TypeMismatch; - } - }, - .record_unbound => |b_fields| { - if (b_fields.len() == 0) { - // Both are empty, merge as empty_record - self.merge(vars, Content{ .structure = .empty_record }); - } else { - return error.TypeMismatch; - } - }, - .record_poly => |b_poly| { - if (b_poly.record.fields.len() == 0) { - try self.unifyGuarded(vars.a.var_, b_poly.record.ext); - } else { - return error.TypeMismatch; - } - }, - else => return error.TypeMismatch, - } - }, - .tag_union => |a_tag_union| { - switch (b_flat_type) { - .empty_tag_union => { - if (a_tag_union.tags.len() == 0) { - try self.unifyGuarded(a_tag_union.ext, vars.b.var_); - } else { - return error.TypeMismatch; - } - }, - .tag_union => |b_tag_union| { - try self.unifyTwoTagUnions(vars, a_tag_union, b_tag_union); - }, - else => return error.TypeMismatch, - } - }, - .empty_tag_union => { - switch (b_flat_type) { - .empty_tag_union => { - self.merge(vars, Content{ .structure = .empty_tag_union }); - }, - .tag_union => |b_tag_union| { - if (b_tag_union.tags.len() == 0) { - try self.unifyGuarded(vars.a.var_, b_tag_union.ext); - } else { - return error.TypeMismatch; - } - }, - else => return error.TypeMismatch, - } - }, - } - } - - /// unify tuples - /// - /// this checks: - /// * that the arities are the same - /// * that parallel arguments unify - fn unifyTuple( - self: *Self, - vars: *const ResolvedVarDescs, - a_tuple: Tuple, - b_tuple: Tuple, - ) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - if (a_tuple.elems.len() != b_tuple.elems.len()) { - return error.TypeMismatch; - } - - const a_elems = self.types_store.sliceVars(a_tuple.elems); - const b_elems = self.types_store.sliceVars(b_tuple.elems); - for (a_elems, b_elems) |a_elem, b_elem| { - try self.unifyGuarded(a_elem, b_elem); - } - - self.merge(vars, vars.b.desc.content); - } - - fn unifyNum( - self: *Self, - vars: *const ResolvedVarDescs, - a_num: Num, - b_num: Num, - ) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - switch (a_num) { - .num_poly => |a_poly| { - switch (b_num) { - .num_poly => |b_poly| { - // Unify the variables - try self.unifyGuarded(a_poly.var_, b_poly.var_); - - // num_poly always contains IntRequirements - self.merge(vars, Content{ .structure = .{ .num = .{ .num_poly = .{ - .var_ = a_poly.var_, - .requirements = a_poly.requirements.unify(b_poly.requirements), - } } } }); - }, - .num_unbound => |b_requirements| { - // When unifying num_poly with num_unbound, the unbound picks up the poly's var - self.merge(vars, Content{ .structure = .{ .num = .{ .num_poly = .{ - .var_ = a_poly.var_, - .requirements = a_poly.requirements.unify(b_requirements), - } } } }); - }, - .num_compact => |b_num_compact| { - // num_poly always contains IntRequirements - switch (b_num_compact) { - .int => |prec| { - const result = self.checkIntPrecisionRequirements(prec, a_poly.requirements); - switch (result) { - .ok => {}, - .negative_unsigned => return error.NegativeUnsignedInt, - .too_large => return error.NumberDoesNotFit, - } - }, - .frac => return error.TypeMismatch, - } - self.merge(vars, vars.b.desc.content); - }, - .int_poly => |b_poly| { - // Both are int requirements - unify and merge - try self.unifyGuarded(a_poly.var_, b_poly.var_); - self.merge(vars, Content{ .structure = .{ .num = .{ .int_poly = .{ - .var_ = a_poly.var_, - .requirements = a_poly.requirements.unify(b_poly.requirements), - } } } }); - }, - .int_unbound => |b_requirements| { - // When unifying int_poly with int_unbound, keep as int_poly - self.merge(vars, Content{ .structure = .{ .num = .{ .int_poly = .{ - .var_ = a_poly.var_, - .requirements = a_poly.requirements.unify(b_requirements), - } } } }); - }, - .frac_poly => { - // num_poly has IntRequirements, frac_poly has FracRequirements - incompatible - return error.TypeMismatch; - }, - .frac_unbound => { - // num_poly has IntRequirements, frac_unbound has FracRequirements - incompatible - return error.TypeMismatch; - }, - .int_precision => |prec| { - // num_poly always contains IntRequirements - const result = self.checkIntPrecisionRequirements(prec, a_poly.requirements); - switch (result) { - .ok => {}, - .negative_unsigned => return error.NegativeUnsignedInt, - .too_large => return error.NumberDoesNotFit, - } - self.merge(vars, vars.b.desc.content); - }, - .frac_precision => { - // num_poly has IntRequirements, frac_precision is for fractions - incompatible - return error.TypeMismatch; - }, - } - }, - .int_poly => |a_poly| { - switch (b_num) { - .num_poly => |b_poly| { - // Both are int requirements - unify and merge - try self.unifyGuarded(a_poly.var_, b_poly.var_); - self.merge(vars, Content{ .structure = .{ .num = .{ .int_poly = .{ - .var_ = a_poly.var_, - .requirements = a_poly.requirements.unify(b_poly.requirements), - } } } }); - }, - .num_unbound => |b_requirements| { - // When unifying int_poly with num_unbound, keep as int_poly - self.merge(vars, Content{ .structure = .{ .num = .{ .int_poly = .{ - .var_ = a_poly.var_, - .requirements = a_poly.requirements.unify(b_requirements), - } } } }); - }, - .int_poly => |b_poly| { - try self.unifyGuarded(a_poly.var_, b_poly.var_); - self.merge(vars, Content{ .structure = .{ .num = .{ .int_poly = .{ - .var_ = a_poly.var_, - .requirements = a_poly.requirements.unify(b_poly.requirements), - } } } }); - }, - .int_unbound => |b_requirements| { - // When unifying int_poly with int_unbound, keep as int_poly - self.merge(vars, Content{ .structure = .{ .num = .{ .int_poly = .{ - .var_ = a_poly.var_, - .requirements = a_poly.requirements.unify(b_requirements), - } } } }); - }, - .int_precision => |prec| { - // Check if the requirements variable is rigid - const req_var_desc = self.module_env.types.resolveVar(a_poly.var_).desc; - if (req_var_desc.content == .rigid_var) { - return error.TypeMismatch; - } - // Check if the precision satisfies the requirements - const result = self.checkIntPrecisionRequirements(prec, a_poly.requirements); - switch (result) { - .ok => {}, - .negative_unsigned => return error.NegativeUnsignedInt, - .too_large => return error.NumberDoesNotFit, - } - self.merge(vars, vars.b.desc.content); - }, - - else => return error.TypeMismatch, - } - }, - .frac_poly => |a_poly| { - switch (b_num) { - .frac_poly => |b_poly| { - try self.unifyGuarded(a_poly.var_, b_poly.var_); - self.merge(vars, Content{ .structure = .{ .num = .{ .frac_poly = .{ - .var_ = a_poly.var_, - .requirements = a_poly.requirements.unify(b_poly.requirements), - } } } }); - }, - .frac_unbound => |b_requirements| { - // When unifying frac_poly with frac_unbound, keep as frac_poly - self.merge(vars, Content{ .structure = .{ .num = .{ .frac_poly = .{ - .var_ = a_poly.var_, - .requirements = a_poly.requirements.unify(b_requirements), - } } } }); - }, - .frac_precision => |prec| { - // Check if the precision satisfies the requirements - if (!self.fracPrecisionSatisfiesRequirements(prec, a_poly.requirements)) { - return error.TypeMismatch; - } - self.merge(vars, vars.b.desc.content); - }, - .num_poly => { - // num_poly has IntRequirements, frac_poly has FracRequirements - incompatible - return error.TypeMismatch; - }, - .num_compact => |b_compact| { - // Check if the requirements variable is rigid - const req_var_desc = self.module_env.types.resolveVar(a_poly.var_).desc; - if (req_var_desc.content == .rigid_var) { - return error.TypeMismatch; - } - // Check if the compact frac type satisfies the requirements - switch (b_compact) { - .frac => |prec| { - if (!self.fracPrecisionSatisfiesRequirements(prec, a_poly.requirements)) { - return error.TypeMismatch; - } - self.merge(vars, vars.b.desc.content); - }, - .int => return error.TypeMismatch, - } - }, - else => return error.TypeMismatch, - } - }, - .num_unbound => |a_requirements| { - switch (b_num) { - .num_poly => |b_poly| { - // When unifying num_unbound with num_poly, the unbound picks up the poly's var - self.merge(vars, Content{ .structure = .{ .num = .{ .num_poly = .{ - .var_ = b_poly.var_, - .requirements = a_requirements.unify(b_poly.requirements), - } } } }); - }, - .num_unbound => |b_requirements| { - // Both unbound - merge requirements, stay unbound - self.merge(vars, Content{ .structure = .{ .num = .{ .num_unbound = a_requirements.unify(b_requirements) } } }); - }, - .int_poly => |b_poly| { - // When unifying num_unbound with int_poly, keep as int_poly - self.merge(vars, Content{ .structure = .{ .num = .{ .int_poly = .{ - .var_ = b_poly.var_, - .requirements = a_requirements.unify(b_poly.requirements), - } } } }); - }, - .int_unbound => |b_requirements| { - // When unifying num_unbound with int_unbound, keep as int_unbound - self.merge(vars, Content{ .structure = .{ .num = .{ .int_unbound = a_requirements.unify(b_requirements) } } }); - }, - .num_compact => |b_num_compact| { - // Check if the compact type satisfies the requirements - switch (b_num_compact) { - .int => |int_prec| { - const result = self.checkIntPrecisionRequirements(int_prec, a_requirements); - switch (result) { - .ok => {}, - .negative_unsigned => return error.NegativeUnsignedInt, - .too_large => return error.NumberDoesNotFit, - } - }, - .frac => return error.TypeMismatch, - } - self.merge(vars, vars.b.desc.content); - }, - .int_precision => |prec| { - // Check if the precision satisfies the requirements - const result = self.checkIntPrecisionRequirements(prec, a_requirements); - switch (result) { - .ok => {}, - .negative_unsigned => return error.NegativeUnsignedInt, - .too_large => return error.NumberDoesNotFit, - } - self.merge(vars, vars.b.desc.content); - }, - .frac_unbound => |b_requirements| { - // When unifying num_unbound with frac_unbound, frac wins - self.merge(vars, Content{ .structure = .{ .num = .{ .frac_unbound = b_requirements } } }); - }, - else => return error.TypeMismatch, - } - }, - .int_unbound => |a_requirements| { - switch (b_num) { - .num_poly => |b_poly| { - // When unifying int_unbound with num_poly, keep as int_poly - self.merge(vars, Content{ .structure = .{ .num = .{ .int_poly = .{ - .var_ = b_poly.var_, - .requirements = a_requirements.unify(b_poly.requirements), - } } } }); - }, - .num_unbound => |b_requirements| { - // When unifying int_unbound with num_unbound, keep as int_unbound - self.merge(vars, Content{ .structure = .{ .num = .{ .int_unbound = a_requirements.unify(b_requirements) } } }); - }, - .int_poly => |b_poly| { - // When unifying int_unbound with int_poly, keep as int_poly - self.merge(vars, Content{ .structure = .{ .num = .{ .int_poly = .{ - .var_ = b_poly.var_, - .requirements = a_requirements.unify(b_poly.requirements), - } } } }); - }, - .int_unbound => |b_requirements| { - // Both int_unbound - merge requirements - self.merge(vars, Content{ .structure = .{ .num = .{ .int_unbound = a_requirements.unify(b_requirements) } } }); - }, - .num_compact => |b_num_compact| { - // Check if it's an int - switch (b_num_compact) { - .int => |int_prec| { - const result = self.checkIntPrecisionRequirements(int_prec, a_requirements); - switch (result) { - .ok => {}, - .negative_unsigned => return error.NegativeUnsignedInt, - .too_large => return error.NumberDoesNotFit, - } - }, - .frac => return error.TypeMismatch, - } - self.merge(vars, vars.b.desc.content); - }, - .int_precision => |prec| { - // Check if the precision satisfies the requirements - const result = self.checkIntPrecisionRequirements(prec, a_requirements); - switch (result) { - .ok => {}, - .negative_unsigned => return error.NegativeUnsignedInt, - .too_large => return error.NumberDoesNotFit, - } - self.merge(vars, vars.b.desc.content); - }, - else => return error.TypeMismatch, - } - }, - .frac_unbound => |a_requirements| { - switch (b_num) { - .frac_poly => |b_poly| { - // When unifying frac_unbound with frac_poly, keep as frac_poly - self.merge(vars, Content{ .structure = .{ .num = .{ .frac_poly = .{ - .var_ = b_poly.var_, - .requirements = a_requirements.unify(b_poly.requirements), - } } } }); - }, - .frac_unbound => |b_requirements| { - // Both frac_unbound - merge requirements - self.merge(vars, Content{ .structure = .{ .num = .{ .frac_unbound = a_requirements.unify(b_requirements) } } }); - }, - .num_compact => |b_num_compact| { - // Check if it's a frac - switch (b_num_compact) { - .frac => |frac_prec| { - if (!self.fracPrecisionSatisfiesRequirements(frac_prec, a_requirements)) { - return error.TypeMismatch; - } - }, - .int => return error.TypeMismatch, - } - self.merge(vars, vars.b.desc.content); - }, - .frac_precision => |prec| { - // Check if the precision satisfies the requirements - if (!self.fracPrecisionSatisfiesRequirements(prec, a_requirements)) { - return error.TypeMismatch; - } - self.merge(vars, vars.b.desc.content); - }, - .num_unbound => |b_requirements| { - // When unifying frac_unbound with num_unbound, frac wins - // Note: b_requirements are IntRequirements, we just keep our FracRequirements - _ = b_requirements; - self.merge(vars, Content{ .structure = .{ .num = .{ .frac_unbound = a_requirements } } }); - }, - else => return error.TypeMismatch, - } - }, - .int_precision => |a_prec| { - switch (b_num) { - .int_precision => |b_prec| { - if (a_prec == b_prec) { - self.merge(vars, vars.b.desc.content); - } else { - return error.TypeMismatch; - } - }, - .num_compact => |b_compact| { - switch (b_compact) { - .int => |b_prec| { - if (a_prec == b_prec) { - self.merge(vars, vars.b.desc.content); - } else { - return error.TypeMismatch; - } - }, - .frac => return error.TypeMismatch, - } - }, - else => return error.TypeMismatch, - } - }, - .frac_precision => |a_prec| { - switch (b_num) { - .frac_precision => |b_prec| { - if (a_prec == b_prec) { - self.merge(vars, vars.b.desc.content); - } else { - return error.TypeMismatch; - } - }, - .num_compact => |b_compact| { - switch (b_compact) { - .frac => |b_prec| { - if (a_prec == b_prec) { - self.merge(vars, vars.b.desc.content); - } else { - return error.TypeMismatch; - } - }, - .int => return error.TypeMismatch, - } - }, - else => return error.TypeMismatch, - } - }, - .num_compact => |a_num_compact| { - switch (b_num) { - .num_compact => |b_num_compact| { - try self.unifyTwoCompactNums(vars, a_num_compact, b_num_compact); - }, - .num_poly => |b_poly| { - // num_poly always contains IntRequirements - switch (a_num_compact) { - .int => |prec| { - const result = self.checkIntPrecisionRequirements(prec, b_poly.requirements); - switch (result) { - .ok => {}, - .negative_unsigned => return error.NegativeUnsignedInt, - .too_large => return error.NumberDoesNotFit, - } - }, - .frac => return error.TypeMismatch, - } - self.merge(vars, vars.a.desc.content); - }, - .int_precision => |b_prec| { - switch (a_num_compact) { - .int => |a_prec| { - if (a_prec == b_prec) { - self.merge(vars, vars.a.desc.content); - } else { - return error.TypeMismatch; - } - }, - .frac => return error.TypeMismatch, - } - }, - .frac_precision => |b_prec| { - switch (a_num_compact) { - .frac => |a_prec| { - if (a_prec == b_prec) { - self.merge(vars, vars.a.desc.content); - } else { - return error.TypeMismatch; - } - }, - .int => return error.TypeMismatch, - } - }, - .frac_poly => |b_poly| { - // Check if the requirements variable is rigid - const req_var_desc = self.module_env.types.resolveVar(b_poly.var_).desc; - if (req_var_desc.content == .rigid_var) { - return error.TypeMismatch; - } - // Check if the compact frac type satisfies the requirements - switch (a_num_compact) { - .frac => |prec| { - if (!self.fracPrecisionSatisfiesRequirements(prec, b_poly.requirements)) { - return error.TypeMismatch; - } - self.merge(vars, vars.a.desc.content); - }, - .int => return error.TypeMismatch, - } - }, - .num_unbound => |b_num_unbound| { - // Check if the compact type satisfies the requirements - switch (a_num_compact) { - .int => |int_prec| { - const result = self.checkIntPrecisionRequirements(int_prec, b_num_unbound); - switch (result) { - .ok => {}, - .negative_unsigned => return error.NegativeUnsignedInt, - .too_large => return error.NumberDoesNotFit, - } - }, - .frac => return error.TypeMismatch, - } - self.merge(vars, vars.a.desc.content); - }, - else => return error.TypeMismatch, - } - }, - } - } - - const IntPrecisionCheckResult = enum { - ok, - negative_unsigned, - too_large, - }; - - fn checkIntPrecisionRequirements(self: *Self, prec: Num.Int.Precision, requirements: Num.IntRequirements) IntPrecisionCheckResult { - _ = self; - - // Check sign requirement - const is_signed = switch (prec) { - .i8, .i16, .i32, .i64, .i128 => true, - .u8, .u16, .u32, .u64, .u128 => false, - }; - - // If we need signed values but have unsigned type, it's a negative literal error - if (requirements.sign_needed and !is_signed) { - return .negative_unsigned; - } - - // Check bits requirement - const available_bits: u8 = switch (prec) { - .i8, .u8 => 8, - .i16, .u16 => 16, - .i32, .u32 => 32, - .i64, .u64 => 64, - .i128, .u128 => 128, - }; - - // Map requirements.bits_needed to actual bit count - const required_bits: u8 = switch (@as(Num.Int.BitsNeeded, @enumFromInt(requirements.bits_needed))) { - .@"7" => 7, - .@"8" => 8, - .@"9_to_15" => 15, - .@"16" => 16, - .@"17_to_31" => 31, - .@"32" => 32, - .@"33_to_63" => 63, - .@"64" => 64, - .@"65_to_127" => 127, - .@"128" => 128, - }; - - // For unsigned types, we need exactly the required bits - if (!is_signed) { - return if (available_bits >= required_bits) .ok else .too_large; - } - - // For signed types, we lose one bit to the sign - const usable_bits = if (is_signed) available_bits - 1 else available_bits; - - return if (usable_bits >= required_bits) .ok else .too_large; - } - - fn intPrecisionSatisfiesRequirements(self: *Self, prec: Num.Int.Precision, requirements: Num.IntRequirements) bool { - return self.checkIntPrecisionRequirements(prec, requirements) == .ok; - } - - fn fracPrecisionSatisfiesRequirements(self: *Self, prec: Num.Frac.Precision, requirements: Num.FracRequirements) bool { - _ = self; - - switch (prec) { - .f32 => return requirements.fits_in_f32, - .f64 => return true, // F64 can always hold values - .dec => return requirements.fits_in_dec, - } - } - - fn unifyTwoCompactNums( - self: *Self, - vars: *const ResolvedVarDescs, - a_num: NumCompact, - b_num: NumCompact, - ) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - switch (a_num) { - .int => |a_int| { - switch (b_num) { - .int => |b_int| if (a_int == b_int) { - self.merge(vars, vars.b.desc.content); - } else { - return error.TypeMismatch; - }, - else => return error.TypeMismatch, - } - }, - .frac => |a_frac| { - switch (b_num) { - .frac => |b_frac| if (a_frac == b_frac) { - self.merge(vars, vars.b.desc.content); - } else { - return error.TypeMismatch; - }, - else => return error.TypeMismatch, - } - }, - } - } - - /// The result of attempting to resolve a polymorphic number - const ResolvedNum = union(enum) { - flex_resolved, - int_resolved: Num.Int.Precision, - frac_resolved: Num.Frac.Precision, - err: Var, - }; - - /// Attempts to resolve a polymorphic number variable to a concrete precision. - /// - /// This function recursively follows the structure of a number type, - /// unwrapping any intermediate `.num_poly`, `.int_poly`, or `.frac_poly` - /// variants until it reaches a concrete representation: - /// either `.int_precision` or `.frac_precision`. - /// - /// For example: - /// Given a type like `Num(Int(U8))`, this function returns `.int_resolved(.u8)`. - /// - /// If resolution reaches a `.flex_var`, it returns `.flex_resolved`, - /// indicating the number is still unspecialized. - /// - /// If the chain ends in an invalid structure (e.g. `Num(Str)`), - /// it returns `.err`, along with the offending variable. - /// TODO: Do we want the chain of offending variables on error? - /// - /// Note that this function will work on the "tail" of a polymorphic number. - /// That is, if you pass in `Frac(Dec)` (without the outer `Num`), this - /// function will still resolve successfully. - fn resolvePolyNum( - self: *Self, - initial_num_var: Var, - ) ResolvedNum { - var num_var = initial_num_var; - while (true) { - const resolved = self.types_store.resolveVar(num_var); - switch (resolved.desc.content) { - .flex_var => return .flex_resolved, - .structure => |flat_type| { - switch (flat_type) { - .num => |num| switch (num) { - .num_poly => |requirements| { - num_var = requirements.var_; - }, - .int_poly => |requirements| { - num_var = requirements.var_; - }, - .frac_poly => |requirements| { - num_var = requirements.var_; - }, - .int_precision => |prec| { - return .{ .int_resolved = prec }; - }, - .frac_precision => |prec| { - return .{ .frac_resolved = prec }; - }, - .num_compact => return .{ .err = num_var }, - }, - else => return .{ .err = num_var }, - } - }, - else => return .{ .err = num_var }, - } - } - } - - // Unify nominal type // - - /// Unify when `a` was a nominal type - fn unifyNominalType(self: *Self, vars: *const ResolvedVarDescs, a_type: NominalType, b_type: NominalType) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - // Check if either nominal type has an invalid backing variable - const a_backing_var = self.types_store.getNominalBackingVar(a_type); - const a_backing_resolved = self.types_store.resolveVar(a_backing_var); - if (a_backing_resolved.desc.content == .err) { - // Invalid nominal type - treat as transparent - self.merge(vars, vars.b.desc.content); - return; - } - - const b_backing_var = self.types_store.getNominalBackingVar(b_type); - const b_backing_resolved = self.types_store.resolveVar(b_backing_var); - if (b_backing_resolved.desc.content == .err) { - // Invalid nominal type - treat as transparent - self.merge(vars, vars.a.desc.content); - return; - } - - if (!TypeIdent.eql(self.module_env.getIdentStore(), a_type.ident, b_type.ident)) { - return error.TypeMismatch; - } - - if (a_type.vars.nonempty.count != b_type.vars.nonempty.count) { - return error.TypeMismatch; - } - - // Unify each pair of arguments using iterators - const a_slice = self.types_store.sliceNominalArgs(a_type); - const b_slice = self.types_store.sliceNominalArgs(b_type); - for (a_slice, b_slice) |a_arg, b_arg| { - try self.unifyGuarded(a_arg, b_arg); - } - - // Note that we *do not* unify backing variable - - self.merge(vars, vars.b.desc.content); - } - - /// unify func - /// - /// this checks: - /// * that the arg arities are the same - /// * that parallel args unify - /// * that ret unifies - fn unifyFunc( - self: *Self, - _: *const ResolvedVarDescs, - a_func: Func, - b_func: Func, - ) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - if (a_func.args.len() != b_func.args.len()) { - return error.TypeMismatch; - } - - const a_args = self.types_store.sliceVars(a_func.args); - const b_args = self.types_store.sliceVars(b_func.args); - for (a_args, b_args) |a_arg, b_arg| { - try self.unifyGuarded(a_arg, b_arg); - } - - try self.unifyGuarded(a_func.ret, b_func.ret); - } - - /// Unify two extensible records. - /// - /// This function implements Elm-style record unification. - /// - /// Each record consists of: - /// - a fixed set of known fields (`fields`) - /// - an extensible tail variable (`ext`) that may point to additional unknown fields - /// - /// Given two records `a` and `b`, we: - /// 1. Collect all known fields by unwrapping their `ext` chains. - /// 2. Partition the field sets into: - /// - `in_both`: shared fields present in both `a` and `b` - /// - `only_in_a`: fields only present in `a` - /// - `only_in_b`: fields only present in `b` - /// 3. Determine the relationship between the two records based on these partitions. - /// - /// Four cases follow: - /// - /// --- - /// - /// **Case 1: Exactly the Same Fields** - /// - /// a = { x, y }ext_a - /// b = { x, y }ext_b - /// - /// - All fields are shared - /// - We unify `ext_a ~ ext_b` - /// - Then unify each shared field pair - /// - /// --- - /// - /// **Case 2: `a` Extends `b`** - /// - /// a = { x, y, z }ext_a - /// b = { x, y }ext_b - /// - /// - `a` has additional fields not in `b` - /// - We generate a new var `only_in_a_var = { z }ext_a` - /// - Unify `only_in_a_var ~ ext_b` - /// - Then unify shared fields - /// - /// --- - /// - /// **Case 3: `b` Extends `a`** - /// - /// a = { x, y }ext_a - /// b = { x, y, z }ext_b - /// - /// - Same as Case 2, but reversed - /// - `b` has additional fields not in `a` - /// - We generate a new var `only_in_b_var = { z }ext_b` - /// - Unify `ext_a ~ only_in_b_var` - /// - Then unify shared fields - /// - /// --- - /// - /// **Case 4: Both Extend Each Other** - /// - /// a = { x, y, z }ext_a - /// b = { x, y, w }ext_b - /// - /// - Each has unique fields the other lacks - /// - Generate: - /// - shared_ext = fresh flex_var - /// - only_in_a_var = { z }shared_ext - /// - only_in_b_var = { w }shared_ext - /// - Unify: - /// - `ext_a ~ only_in_b_var` - /// - `only_in_a_var ~ ext_b` - /// - Then unify shared fields into `{ x, y }shared_ext` - /// - /// --- - /// - /// All field unification is done using `unifySharedFields`, and new variables are created using `fresh`. - /// - /// This function does not attempt to deduplicate fields or reorder them — callers are responsible - /// for providing consistent field names. - fn unifyTwoRecords( - self: *Self, - vars: *const ResolvedVarDescs, - a_record: Record, - b_record: Record, - ) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - // First, unwrap all fields for record, erroring if we encounter an - // invalid record ext var - const a_gathered_fields = try self.gatherRecordFields(a_record); - const b_gathered_fields = try self.gatherRecordFields(b_record); - - // Then partition the fields - const partitioned = Self.partitionFields( - self.module_env.getIdentStore(), - self.scratch, - a_gathered_fields.range, - b_gathered_fields.range, - ) catch return Error.AllocatorError; - - // Determine how the fields of a & b extend - const a_has_uniq_fields = partitioned.only_in_a.len() > 0; - const b_has_uniq_fields = partitioned.only_in_b.len() > 0; - - var fields_ext: FieldsExtension = .exactly_the_same; - if (a_has_uniq_fields and b_has_uniq_fields) { - fields_ext = .both_extend; - } else if (a_has_uniq_fields) { - fields_ext = .a_extends_b; - } else if (b_has_uniq_fields) { - fields_ext = .b_extends_a; - } - - // Unify fields - switch (fields_ext) { - .exactly_the_same => { - // Unify exts - try self.unifyGuarded(a_gathered_fields.ext, b_gathered_fields.ext); - - // Unify shared fields - // This copies fields from scratch into type_store - try self.unifySharedFields( - vars, - self.scratch.in_both_fields.sliceRange(partitioned.in_both), - null, - null, - a_gathered_fields.ext, - ); - }, - .a_extends_b => { - // Create a new variable of a record with only a's uniq fields - // This copies fields from scratch into type_store - const only_in_a_fields_range = self.types_store.appendRecordFields( - self.scratch.only_in_a_fields.sliceRange(partitioned.only_in_a), - ) catch return Error.AllocatorError; - const only_in_a_var = self.fresh(vars, Content{ .structure = FlatType{ .record = .{ - .fields = only_in_a_fields_range, - .ext = a_gathered_fields.ext, - } } }) catch return Error.AllocatorError; - - // Unify the sub record with b's ext - try self.unifyGuarded(only_in_a_var, b_gathered_fields.ext); - - // Unify shared fields - // This copies fields from scratch into type_store - try self.unifySharedFields( - vars, - self.scratch.in_both_fields.sliceRange(partitioned.in_both), - null, - null, - only_in_a_var, - ); - }, - .b_extends_a => { - // Create a new variable of a record with only b's uniq fields - // This copies fields from scratch into type_store - const only_in_b_fields_range = self.types_store.appendRecordFields( - self.scratch.only_in_b_fields.sliceRange(partitioned.only_in_b), - ) catch return Error.AllocatorError; - const only_in_b_var = self.fresh(vars, Content{ .structure = FlatType{ .record = .{ - .fields = only_in_b_fields_range, - .ext = b_gathered_fields.ext, - } } }) catch return Error.AllocatorError; - - // Unify the sub record with a's ext - try self.unifyGuarded(a_gathered_fields.ext, only_in_b_var); - - // Unify shared fields - // This copies fields from scratch into type_store - try self.unifySharedFields( - vars, - self.scratch.in_both_fields.sliceRange(partitioned.in_both), - null, - null, - only_in_b_var, - ); - }, - .both_extend => { - // Create a new variable of a record with only a's uniq fields - // This copies fields from scratch into type_store - const only_in_a_fields_range = self.types_store.appendRecordFields( - self.scratch.only_in_a_fields.sliceRange(partitioned.only_in_a), - ) catch return Error.AllocatorError; - const only_in_a_var = self.fresh(vars, Content{ .structure = FlatType{ .record = .{ - .fields = only_in_a_fields_range, - .ext = a_gathered_fields.ext, - } } }) catch return Error.AllocatorError; - - // Create a new variable of a record with only b's uniq fields - // This copies fields from scratch into type_store - const only_in_b_fields_range = self.types_store.appendRecordFields( - self.scratch.only_in_b_fields.sliceRange(partitioned.only_in_b), - ) catch return Error.AllocatorError; - const only_in_b_var = self.fresh(vars, Content{ .structure = FlatType{ .record = .{ - .fields = only_in_b_fields_range, - .ext = b_gathered_fields.ext, - } } }) catch return Error.AllocatorError; - - // Create a new ext var - const new_ext_var = self.fresh(vars, .{ .flex_var = null }) catch return Error.AllocatorError; - - // Unify the sub records with exts - try self.unifyGuarded(a_gathered_fields.ext, only_in_b_var); - try self.unifyGuarded(only_in_a_var, b_gathered_fields.ext); - - // Unify shared fields - // This copies fields from scratch into type_store - try self.unifySharedFields( - vars, - self.scratch.in_both_fields.sliceRange(partitioned.in_both), - self.scratch.only_in_a_fields.sliceRange(partitioned.only_in_a), - self.scratch.only_in_b_fields.sliceRange(partitioned.only_in_b), - new_ext_var, - ); - }, - } - } - - const FieldsExtension = enum { exactly_the_same, a_extends_b, b_extends_a, both_extend }; - - const GatheredFields = struct { ext: Var, range: RecordFieldSafeList.Range }; - - /// Recursively unwraps the fields of an extensible record, flattening all visible fields - /// into `scratch.gathered_fields` and following through: - /// * aliases (by chasing `.backing_var`) - /// * record extension chains (via nested `.record.ext`) - /// - /// Returns: - /// * a `Range` indicating the location of the gathered fields in `gathered_fields` - /// * the final tail extension variable, which is either a flex var or an empty record - /// - /// Errors if it encounters a malformed or invalid extension (e.g. a non-record type). - fn gatherRecordFields(self: *Self, record: Record) Error!GatheredFields { - // first, copy from the store's MultiList record fields array into scratch's - // regular list, capturing the insertion range - var range = self.scratch.copyGatherFieldsFromMultiList( - &self.types_store.record_fields, - record.fields, - ) catch return Error.AllocatorError; - - // then recursiv - var ext_var = record.ext; - while (true) { - switch (self.types_store.resolveVar(ext_var).desc.content) { - .flex_var => { - return .{ .ext = ext_var, .range = range }; - }, - .rigid_var => { - return .{ .ext = ext_var, .range = range }; - }, - .alias => |alias| { - ext_var = self.types_store.getAliasBackingVar(alias); - }, - .structure => |flat_type| { - switch (flat_type) { - .record => |ext_record| { - const next_range = self.scratch.copyGatherFieldsFromMultiList( - &self.types_store.record_fields, - ext_record.fields, - ) catch return Error.AllocatorError; - range.count += next_range.count; - ext_var = ext_record.ext; - }, - .record_unbound => |fields| { - const next_range = self.scratch.copyGatherFieldsFromMultiList( - &self.types_store.record_fields, - fields, - ) catch return Error.AllocatorError; - range.count += next_range.count; - // record_unbound has no extension, so we're done - return .{ .ext = ext_var, .range = range }; - }, - .record_poly => |poly| { - const next_range = self.scratch.copyGatherFieldsFromMultiList( - &self.types_store.record_fields, - poly.record.fields, - ) catch return Error.AllocatorError; - range.count += next_range.count; - ext_var = poly.record.ext; - }, - .empty_record => { - return .{ .ext = ext_var, .range = range }; - }, - else => try self.setUnifyErrAndThrow(.{ .invalid_record_ext = ext_var }), - } - }, - else => try self.setUnifyErrAndThrow(.{ .invalid_record_ext = ext_var }), - } - } - } - - const PartitionedRecordFields = struct { - only_in_a: RecordFieldSafeList.Range, - only_in_b: RecordFieldSafeList.Range, - in_both: TwoRecordFieldsSafeList.Range, - }; - - /// Given two ranges of record fields stored in `scratch.gathered_fields`, this function: - /// * sorts both slices in-place by field name - /// * partitions them into three disjoint groups: - /// - fields only in `a` - /// - fields only in `b` - /// - fields present in both (by name) - /// - /// These groups are stored into dedicated scratch buffers: - /// * `only_in_a_fields` - /// * `only_in_b_fields` - /// * `in_both_fields` - /// - /// The result is a set of ranges that can be used to slice those buffers. - /// - /// The caller must not mutate the field ranges between `gatherRecordFields` and `partitionFields`. - fn partitionFields( - ident_store: *const Ident.Store, - scratch: *Scratch, - a_fields_range: RecordFieldSafeList.Range, - b_fields_range: RecordFieldSafeList.Range, - ) std.mem.Allocator.Error!PartitionedRecordFields { - // First sort the fields - const a_fields = scratch.gathered_fields.sliceRange(a_fields_range); - std.mem.sort(RecordField, a_fields, ident_store, comptime RecordField.sortByNameAsc); - const b_fields = scratch.gathered_fields.sliceRange(b_fields_range); - std.mem.sort(RecordField, b_fields, ident_store, comptime RecordField.sortByNameAsc); - - // Get the start of index of the new range - const a_fields_start: u32 = @intCast(scratch.only_in_a_fields.len()); - const b_fields_start: u32 = @intCast(scratch.only_in_b_fields.len()); - const both_fields_start: u32 = @intCast(scratch.in_both_fields.len()); - - // Iterate over the fields in order, grouping them - var a_i: usize = 0; - var b_i: usize = 0; - while (a_i < a_fields.len and b_i < b_fields.len) { - const a_next = a_fields[a_i]; - const b_next = b_fields[b_i]; - const ord = RecordField.orderByName(ident_store, a_next, b_next); - switch (ord) { - .eq => { - _ = try scratch.in_both_fields.append(scratch.gpa, TwoRecordFields{ - .a = a_next, - .b = b_next, - }); - a_i = a_i + 1; - b_i = b_i + 1; - }, - .lt => { - _ = try scratch.only_in_a_fields.append(scratch.gpa, a_next); - a_i = a_i + 1; - }, - .gt => { - _ = try scratch.only_in_b_fields.append(scratch.gpa, b_next); - b_i = b_i + 1; - }, - } - } - - // If b was shorter, add the extra a elems - while (a_i < a_fields.len) { - const a_next = a_fields[a_i]; - _ = try scratch.only_in_a_fields.append(scratch.gpa, a_next); - a_i = a_i + 1; - } - - // If a was shorter, add the extra b elems - while (b_i < b_fields.len) { - const b_next = b_fields[b_i]; - _ = try scratch.only_in_b_fields.append(scratch.gpa, b_next); - b_i = b_i + 1; - } - - // Return the ranges - return .{ - .only_in_a = scratch.only_in_a_fields.rangeToEnd(a_fields_start), - .only_in_b = scratch.only_in_b_fields.rangeToEnd(b_fields_start), - .in_both = scratch.in_both_fields.rangeToEnd(both_fields_start), - }; - } - - /// Given a list of shared fields & a list of extended fields, unify the shared - /// Then merge a new record with both shared+extended fields - fn unifySharedFields( - self: *Self, - vars: *const ResolvedVarDescs, - shared_fields: TwoRecordFieldsSafeList.Slice, - mb_a_extended_fields: ?RecordFieldSafeList.Slice, - mb_b_extended_fields: ?RecordFieldSafeList.Slice, - ext: Var, - ) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - const range_start: u32 = self.types_store.record_fields.len(); - - // Here, iterate over shared fields, sub unifying the field variables. - // At this point, the fields are know to be identical, so we arbitrary choose b - for (shared_fields) |shared| { - try self.unifyGuarded(shared.a.var_, shared.b.var_); - _ = self.types_store.appendRecordFields(&[_]RecordField{.{ - .name = shared.b.name, - .var_ = shared.b.var_, - }}) catch return Error.AllocatorError; - } - - // Append combined fields - if (mb_a_extended_fields) |extended_fields| { - _ = self.types_store.appendRecordFields(extended_fields) catch return Error.AllocatorError; - } - if (mb_b_extended_fields) |extended_fields| { - _ = self.types_store.appendRecordFields(extended_fields) catch return Error.AllocatorError; - } - - // Merge vars - self.merge(vars, Content{ .structure = FlatType{ .record = .{ - .fields = self.types_store.record_fields.rangeToEnd(range_start), - .ext = ext, - } } }); - } - - /// Unify two extensible tag union. - /// - /// This function implements Elm-style record unification, but for tag unions. - /// - /// Each tag union consists of: - /// - a fixed set of known tags (`tags`) - /// - an extensible tail variable (`ext`) that may point to additional unknown tags - /// - /// Given two tag unions `a` and `b`, we: - /// 1. Collect all known tags by unwrapping their `ext` chains. - /// 2. Partition the tags sets into: - /// - `in_both`: shared fields present in both `a` and `b` - /// - `only_in_a`: fields only present in `a` - /// - `only_in_b`: fields only present in `b` - /// 3. Determine the relationship between the two tag unions based on these partitions. - /// - /// Four cases follow: - /// - /// --- - /// - /// **Case 1: Exactly the Same Tags** - /// - /// a = [ X ]ext_a - /// b = [ X ]ext_b - /// - /// - All tags are shared - /// - We unify `ext_a ~ ext_b` - /// - Then unify each shared tag pair - /// - /// --- - /// - /// **Case 2: `a` Extends `b`** - /// - /// a = [ X, Y, Z ]ext_a - /// b = [ X, Y ]ext_b - /// - /// - `a` has additional tags not in `b` - /// - We generate a new var `only_in_a_var = [ Z ]ext_a` - /// - Unify `only_in_a_var ~ ext_b` - /// - Then unify shared tags into `[ X, Y ]only_in_a_var` - /// - /// --- - /// - /// **Case 3: `b` Extends `a`** - /// - /// a = [ X, Y ]ext_a - /// b = [ X, Y, Z ]ext_b - /// - /// - Same as Case 2, but reversed - /// - `b` has additional tags not in `a` - /// - We generate a new var `only_in_b_var = [ Z ]ext_b` - /// - Unify `ext_a ~ only_in_b_var` - /// - Then unify shared tags into `[ X, Y ]only_in_b_var` - /// - /// --- - /// - /// **Case 4: Both Extend Each Other** - /// - /// a = [ X, Y, Z ]ext_a - /// b = [ X, Y, W ]ext_b - /// - /// - Each has unique tags the other lacks - /// - Generate: - /// - shared_ext = fresh flex_var - /// - only_in_a_var = [ Z ]shared_ext - /// - only_in_b_var = [ W ]shared_ext - /// - Unify: - /// - `ext_a ~ only_in_b_var` - /// - `only_in_a_var ~ ext_b` - /// - Then unify shared tags into `[ X, Y ]shared_ext` - /// - /// --- - /// - /// All tag unification is done using `unifySharedTags`, and new variables are created using `fresh`. - /// - /// This function does not attempt to deduplicate tags or reorder them — callers are responsible - /// for providing consistent tag names. - fn unifyTwoTagUnions( - self: *Self, - vars: *const ResolvedVarDescs, - a_tag_union: TagUnion, - b_tag_union: TagUnion, - ) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - // First, unwrap all fields for tag unions, erroring if we encounter an - // invalid record ext var - const a_gathered_tags = try self.gatherTagUnionTags(a_tag_union); - const b_gathered_tags = try self.gatherTagUnionTags(b_tag_union); - - // Then partition the tags - const partitioned = Self.partitionTags( - self.module_env.getIdentStore(), - self.scratch, - a_gathered_tags.range, - b_gathered_tags.range, - ) catch return Error.AllocatorError; - - // Determine how the tags of a & b extend - const a_has_uniq_tags = partitioned.only_in_a.len() > 0; - const b_has_uniq_tags = partitioned.only_in_b.len() > 0; - - var tags_ext: TagsExtension = .exactly_the_same; - if (a_has_uniq_tags and b_has_uniq_tags) { - tags_ext = .both_extend; - } else if (a_has_uniq_tags) { - tags_ext = .a_extends_b; - } else if (b_has_uniq_tags) { - tags_ext = .b_extends_a; - } - - // Unify tags - switch (tags_ext) { - .exactly_the_same => { - // Unify exts - try self.unifyGuarded(a_gathered_tags.ext, b_gathered_tags.ext); - - // Unify shared tags - // This copies tags from scratch into type_store - try self.unifySharedTags( - vars, - self.scratch.in_both_tags.sliceRange(partitioned.in_both), - null, - null, - a_gathered_tags.ext, - ); - }, - .a_extends_b => { - // Create a new variable of a tag_union with only a's uniq tags - // This copies tags from scratch into type_store - const only_in_a_tags_range = self.types_store.appendTags( - self.scratch.only_in_a_tags.sliceRange(partitioned.only_in_a), - ) catch return Error.AllocatorError; - const only_in_a_var = self.fresh(vars, Content{ .structure = FlatType{ .tag_union = .{ - .tags = only_in_a_tags_range, - .ext = a_gathered_tags.ext, - } } }) catch return Error.AllocatorError; - - // Unify the sub tag_union with b's ext - try self.unifyGuarded(only_in_a_var, b_gathered_tags.ext); - - // Unify shared tags - // This copies tags from scratch into type_store - try self.unifySharedTags( - vars, - self.scratch.in_both_tags.sliceRange(partitioned.in_both), - null, - null, - only_in_a_var, - ); - }, - .b_extends_a => { - // Create a new variable of a tag_union with only b's uniq tags - // This copies tags from scratch into type_store - const only_in_b_tags_range = self.types_store.appendTags( - self.scratch.only_in_b_tags.sliceRange(partitioned.only_in_b), - ) catch return Error.AllocatorError; - const only_in_b_var = self.fresh(vars, Content{ .structure = FlatType{ .tag_union = .{ - .tags = only_in_b_tags_range, - .ext = b_gathered_tags.ext, - } } }) catch return Error.AllocatorError; - - // Unify the sub tag_union with a's ext - try self.unifyGuarded(a_gathered_tags.ext, only_in_b_var); - - // Unify shared tags - // This copies tags from scratch into type_store - try self.unifySharedTags( - vars, - self.scratch.in_both_tags.sliceRange(partitioned.in_both), - null, - null, - only_in_b_var, - ); - }, - .both_extend => { - // Create a new variable of a tag_union with only a's uniq tags - // This copies tags from scratch into type_store - const only_in_a_tags_range = self.types_store.appendTags( - self.scratch.only_in_a_tags.sliceRange(partitioned.only_in_a), - ) catch return Error.AllocatorError; - const only_in_a_var = self.fresh(vars, Content{ .structure = FlatType{ .tag_union = .{ - .tags = only_in_a_tags_range, - .ext = a_gathered_tags.ext, - } } }) catch return Error.AllocatorError; - - // Create a new variable of a tag_union with only b's uniq tags - // This copies tags from scratch into type_store - const only_in_b_tags_range = self.types_store.appendTags( - self.scratch.only_in_b_tags.sliceRange(partitioned.only_in_b), - ) catch return Error.AllocatorError; - const only_in_b_var = self.fresh(vars, Content{ .structure = FlatType{ .tag_union = .{ - .tags = only_in_b_tags_range, - .ext = b_gathered_tags.ext, - } } }) catch return Error.AllocatorError; - - // Create a new ext var - const new_ext_var = self.fresh(vars, .{ .flex_var = null }) catch return Error.AllocatorError; - - // Unify the sub tag_unions with exts - try self.unifyGuarded(a_gathered_tags.ext, only_in_b_var); - try self.unifyGuarded(only_in_a_var, b_gathered_tags.ext); - - // Unify shared tags - // This copies tags from scratch into type_store - try self.unifySharedTags( - vars, - self.scratch.in_both_tags.sliceRange(partitioned.in_both), - self.scratch.only_in_a_tags.sliceRange(partitioned.only_in_a), - self.scratch.only_in_b_tags.sliceRange(partitioned.only_in_b), - new_ext_var, - ); - }, - } - } - - const TagsExtension = enum { exactly_the_same, a_extends_b, b_extends_a, both_extend }; - - const GatheredTags = struct { ext: Var, range: TagSafeList.Range }; - - /// Recursively unwraps the tags of an extensible tag_union, flattening all visible tags - /// into `scratch.gathered_tags` and following through: - /// * aliases (by chasing `.backing_var`) - /// * tag_union extension chains (via nested `.tag_union.ext`) - /// - /// Returns: - /// * a `Range` indicating the location of the gathered tags in `gathered_tags` - /// * the final tail extension variable, which is either a flex var or an empty tag_union - /// - /// Errors if it encounters a malformed or invalid extension (e.g. a non-tag_union type). - fn gatherTagUnionTags(self: *Self, tag_union: TagUnion) Error!GatheredTags { - // first, copy from the store's MultiList record fields array into scratch's - // regular list, capturing the insertion range - var range = self.scratch.copyGatherTagsFromMultiList( - &self.types_store.tags, - tag_union.tags, - ) catch return Error.AllocatorError; - - // then loop gathering extensible tags - var ext_var = tag_union.ext; - while (true) { - switch (self.types_store.resolveVar(ext_var).desc.content) { - .flex_var => { - return .{ .ext = ext_var, .range = range }; - }, - .rigid_var => { - return .{ .ext = ext_var, .range = range }; - }, - .alias => |alias| { - ext_var = self.types_store.getAliasBackingVar(alias); - }, - .structure => |flat_type| { - switch (flat_type) { - .tag_union => |ext_tag_union| { - const next_range = self.scratch.copyGatherTagsFromMultiList( - &self.types_store.tags, - ext_tag_union.tags, - ) catch return Error.AllocatorError; - range.count += next_range.count; - ext_var = ext_tag_union.ext; - }, - .empty_tag_union => { - return .{ .ext = ext_var, .range = range }; - }, - else => try self.setUnifyErrAndThrow(.{ .invalid_tag_union_ext = ext_var }), - } - }, - else => try self.setUnifyErrAndThrow(.{ .invalid_tag_union_ext = ext_var }), - } - } - } - - const PartitionedTags = struct { - only_in_a: TagSafeList.Range, - only_in_b: TagSafeList.Range, - in_both: TwoTagsSafeList.Range, - }; - - /// Given two ranges of tag_union tags stored in `scratch.gathered_tags`, this function: - /// * sorts both slices in-place by field name - /// * partitions them into three disjoint groups: - /// - tags only in `a` - /// - tags only in `b` - /// - tags present in both (by name) - /// - /// These groups are stored into dedicated scratch buffers: - /// * `only_in_a_tags` - /// * `only_in_b_tags` - /// * `in_both_tags` - /// - /// The result is a set of ranges that can be used to slice those buffers. - /// - /// The caller must not mutate the field ranges between `gatherTagUnionTags` and `partitionTags`. - fn partitionTags( - ident_store: *const Ident.Store, - scratch: *Scratch, - a_tags_range: TagSafeList.Range, - b_tags_range: TagSafeList.Range, - ) std.mem.Allocator.Error!PartitionedTags { - // First sort the tags - const a_tags = scratch.gathered_tags.sliceRange(a_tags_range); - std.mem.sort(Tag, a_tags, ident_store, comptime Tag.sortByNameAsc); - const b_tags = scratch.gathered_tags.sliceRange(b_tags_range); - std.mem.sort(Tag, b_tags, ident_store, comptime Tag.sortByNameAsc); - - // Get the start of index of the new range - const a_tags_start: u32 = @intCast(scratch.only_in_a_tags.len()); - const b_tags_start: u32 = @intCast(scratch.only_in_b_tags.len()); - const both_tags_start: u32 = @intCast(scratch.in_both_tags.len()); - - // Iterate over the tags in order, grouping them - var a_i: usize = 0; - var b_i: usize = 0; - while (a_i < a_tags.len and b_i < b_tags.len) { - const a_next = a_tags[a_i]; - const b_next = b_tags[b_i]; - const ord = Tag.orderByName(ident_store, a_next, b_next); - switch (ord) { - .eq => { - _ = try scratch.in_both_tags.append(scratch.gpa, TwoTags{ .a = a_next, .b = b_next }); - a_i = a_i + 1; - b_i = b_i + 1; - }, - .lt => { - _ = try scratch.only_in_a_tags.append(scratch.gpa, a_next); - a_i = a_i + 1; - }, - .gt => { - _ = try scratch.only_in_b_tags.append(scratch.gpa, b_next); - b_i = b_i + 1; - }, - } - } - - // If b was shorter, add the extra a elems - while (a_i < a_tags.len) { - const a_next = a_tags[a_i]; - _ = try scratch.only_in_a_tags.append(scratch.gpa, a_next); - a_i = a_i + 1; - } - - // If a was shorter, add the extra b elems - while (b_i < b_tags.len) { - const b_next = b_tags[b_i]; - _ = try scratch.only_in_b_tags.append(scratch.gpa, b_next); - b_i = b_i + 1; - } - - // Return the ranges - return .{ - .only_in_a = scratch.only_in_a_tags.rangeToEnd(a_tags_start), - .only_in_b = scratch.only_in_b_tags.rangeToEnd(b_tags_start), - .in_both = scratch.in_both_tags.rangeToEnd(both_tags_start), - }; - } - - /// Given a list of shared tags & a list of extended tags, unify the shared tags. - /// Then merge a new tag_union with both shared+extended tags - fn unifySharedTags( - self: *Self, - vars: *const ResolvedVarDescs, - shared_tags: []TwoTags, - mb_a_extended_tags: ?[]Tag, - mb_b_extended_tags: ?[]Tag, - ext: Var, - ) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - - const range_start: u32 = self.types_store.tags.len(); - - for (shared_tags) |tags| { - const tag_a_args = self.types_store.sliceVars(tags.a.args); - const tag_b_args = self.types_store.sliceVars(tags.b.args); - - if (tag_a_args.len != tag_b_args.len) return error.TypeMismatch; - - for (tag_a_args, tag_b_args) |a_arg, b_arg| { - try self.unifyGuarded(a_arg, b_arg); - } - - _ = self.types_store.appendTags(&[_]Tag{.{ - .name = tags.b.name, - .args = tags.b.args, - }}) catch return Error.AllocatorError; - } - - // Append combined tags - if (mb_a_extended_tags) |extended_tags| { - _ = self.types_store.appendTags(extended_tags) catch return Error.AllocatorError; - } - if (mb_b_extended_tags) |extended_tags| { - _ = self.types_store.appendTags(extended_tags) catch return Error.AllocatorError; - } - - // Merge vars - self.merge(vars, Content{ .structure = FlatType{ .tag_union = .{ - .tags = self.types_store.tags.rangeToEnd(range_start), - .ext = ext, - } } }); - } - - /// Set error data in scratch & throw - fn setUnifyErrAndThrow(self: *Self, err: UnifyErrCtx) Error!void { - self.scratch.setUnifyErr(err); - return error.UnifyErr; - } + } + + // merge + + /// Link the variables & updated the content in the type_store + /// In the old compiler, this function was called "merge" + fn merge(self: *Self, vars: *const ResolvedVarDescs, new_content: Content) void { + self.types_store.union_(vars.a.var_, vars.b.var_, .{ + .content = new_content, + .rank = Rank.min(vars.a.desc.rank, vars.b.desc.rank), + .mark = Mark.none, + }); + } + + /// Create a new type variable *in this pool* + fn fresh(self: *Self, vars: *const ResolvedVarDescs, new_content: Content) std.mem.Allocator.Error!Var { + const var_ = try self.types_store.register(.{ + .content = new_content, + .rank = Rank.min(vars.a.desc.rank, vars.b.desc.rank), + .mark = Mark.none, + }); + _ = try self.scratch.fresh_vars.append(self.scratch.gpa, var_); + return var_; + } + + // unification + + const Error = error{ + TypeMismatch, + UnifyErr, + NumberDoesNotFit, + NegativeUnsignedInt, + AllocatorError, }; -} + + const NominalDirection = enum { + a_is_nominal, + b_is_nominal, + }; + + const max_depth_before_occurs = 8; + + fn unifyGuarded(self: *Self, a_var: Var, b_var: Var) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + switch (self.types_store.checkVarsEquiv(a_var, b_var)) { + .equiv => { + // this means that the vars point to the same exact type + // descriptor, so nothing needs to happen + return; + }, + .not_equiv => |vars| { + if (self.skip_depth_check or self.depth < max_depth_before_occurs) { + self.depth += 1; + const result = self.unifyVars(&vars); + self.depth -= 1; + _ = try result; + } else { + try self.checkRecursive(&vars); + + self.skip_depth_check = true; + try self.unifyVars(&vars); + self.skip_depth_check = false; + } + }, + } + } + + /// Unify two vars + /// Internal entry point for unification logic. Use `unifyGuarded` to ensure + /// proper depth tracking and occurs checking. + fn unifyVars(self: *Self, vars: *const ResolvedVarDescs) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + switch (vars.a.desc.content) { + .flex => |flex| { + try self.unifyFlex(vars, flex, vars.b.desc.content); + }, + .rigid => |rigid| { + try self.unifyRigid(vars, rigid, vars.b.desc.content); + }, + .alias => |a_alias| { + try self.unifyAlias(vars, a_alias, vars.b.desc.content); + }, + .structure => |a_flat_type| { + try self.unifyStructure(vars, a_flat_type, vars.b.desc.content); + }, + .recursion_var => |a_rec_var| { + try self.unifyRecursionVar(vars, a_rec_var, vars.b.desc.content); + }, + .err => self.merge(vars, .err), + } + } + + /// Run a full occurs check on each variable, erroring if it is infinite + /// or anonymous recursion + /// + /// This function is called when unify has recursed a sufficient depth that + /// a recursive type seems likely. + fn checkRecursive(self: *Self, vars: *const ResolvedVarDescs) Error!void { + const a_occurs = occurs.occurs(self.types_store, self.occurs_scratch, vars.a.var_) catch return Error.AllocatorError; + switch (a_occurs) { + .not_recursive => {}, + .recursive_nominal => {}, + .recursive_anonymous => { + return self.setUnifyErrAndThrow(UnifyErrCtx{ .recursion_anonymous = vars.a.var_ }); + }, + .infinite => { + return self.setUnifyErrAndThrow(UnifyErrCtx{ .recursion_infinite = vars.a.var_ }); + }, + } + + const b_occurs = occurs.occurs(self.types_store, self.occurs_scratch, vars.b.var_) catch return Error.AllocatorError; + switch (b_occurs) { + .not_recursive => {}, + .recursive_nominal => {}, + .recursive_anonymous => { + return self.setUnifyErrAndThrow(UnifyErrCtx{ .recursion_anonymous = vars.b.var_ }); + }, + .infinite => { + return self.setUnifyErrAndThrow(UnifyErrCtx{ .recursion_infinite = vars.b.var_ }); + }, + } + } + + // Unify flex // + + /// Unify when `a` was a flex + fn unifyFlex(self: *Self, vars: *const ResolvedVarDescs, a_flex: Flex, b_content: Content) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + switch (b_content) { + .flex => |b_flex| { + const mb_ident = blk: { + if (a_flex.name) |a_ident| { + break :blk a_ident; + } else { + break :blk b_flex.name; + } + }; + + const merged_constraints = try self.unifyStaticDispatchConstraints(a_flex.constraints, b_flex.constraints); + self.merge(vars, Content{ .flex = .{ + .name = mb_ident, + .constraints = merged_constraints, + } }); + }, + .rigid => |b_rigid| { + if (a_flex.constraints.len() > 0) { + // Record that we need to check constraints later + _ = self.scratch.deferred_constraints.append(self.scratch.gpa, DeferredConstraintCheck{ + .var_ = vars.b.var_, // Since the vars are merge, we arbitrary choose b + .constraints = a_flex.constraints, + }) catch return Error.AllocatorError; + } + + self.merge(vars, .{ .rigid = b_rigid }); + }, + .alias => |b_alias| { + if (a_flex.constraints.len() == 0) { + self.merge(vars, b_content); + } else { + // Merge against backing var, so we don't loose static dispatch constraints + const backing_var = self.types_store.getAliasBackingVar(b_alias); + try self.unifyGuarded(vars.a.var_, backing_var); + } + }, + .structure => { + if (a_flex.constraints.len() > 0) { + // Record that we need to check constraints later + _ = self.scratch.deferred_constraints.append(self.scratch.gpa, DeferredConstraintCheck{ + .var_ = vars.b.var_, // Since the vars are merge, we arbitrary choose b + .constraints = a_flex.constraints, + }) catch return Error.AllocatorError; + } + + self.merge(vars, b_content); + }, + .recursion_var => { + if (a_flex.constraints.len() > 0) { + // Record that we need to check constraints later + _ = self.scratch.deferred_constraints.append(self.scratch.gpa, DeferredConstraintCheck{ + .var_ = vars.b.var_, + .constraints = a_flex.constraints, + }) catch return Error.AllocatorError; + } + + self.merge(vars, b_content); + }, + .err => self.merge(vars, .err), + } + } + + // Unify rigid // + + /// Unify when `a` was a rigid + fn unifyRigid(self: *Self, vars: *const ResolvedVarDescs, a_rigid: Rigid, b_content: Content) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + switch (b_content) { + .flex => |b_flex| { + if (b_flex.constraints.len() > 0) { + // Record that we need to check constraints later + _ = self.scratch.deferred_constraints.append(self.scratch.gpa, DeferredConstraintCheck{ + .var_ = vars.b.var_, // Since the vars are merge, we arbitrary choose b + .constraints = b_flex.constraints, + }) catch return Error.AllocatorError; + } + + self.merge(vars, .{ .rigid = a_rigid }); + }, + .rigid => return error.TypeMismatch, + .alias => return error.TypeMismatch, + .structure => return error.TypeMismatch, + .recursion_var => return error.TypeMismatch, + .err => self.merge(vars, .err), + } + } + + // Unify alias // + + /// Unify when `a` was a alias + fn unifyAlias(self: *Self, vars: *const ResolvedVarDescs, a_alias: Alias, b_content: Content) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + const backing_var = self.types_store.getAliasBackingVar(a_alias); + + switch (b_content) { + .flex => |b_flex| { + if (b_flex.constraints.len() == 0) { + self.merge(vars, Content{ .alias = a_alias }); + } else { + // Merge against backing var, so we don't loose static dispatch constraints + try self.unifyGuarded(backing_var, vars.b.var_); + } + }, + .rigid => |_| { + try self.unifyGuarded(backing_var, vars.b.var_); + }, + .alias => |b_alias| { + const b_backing_var = self.types_store.getAliasBackingVar(b_alias); + if (TypeIdent.eql(self.module_env.getIdentStore(), a_alias.ident, b_alias.ident)) { + try self.unifyTwoAliases(vars, a_alias, b_alias); + } else { + try self.unifyGuarded(backing_var, b_backing_var); + } + }, + .structure => { + // When unifying an alias with a concrete structure, we + // want to preserve the alias for display while ensuring the + // types are compatible. + + // First, we unify the concrete var with the alias backing var + // IMPORTANT: The arg order here is important! Unifying + // updates the second var to hold the type, and the first + // var to redirect to the second + try self.unifyGuarded(vars.b.var_, backing_var); + + // Next, we create a fresh alias (which internally points to `backing_var`), + // then we redirect both a & b to the new alias. + const fresh_alias_var = self.fresh(vars, .{ .alias = a_alias }) catch return Error.AllocatorError; + + // These redirects are safe because fresh_alias_var is created at min(a_rank, b_rank). + // Because of this, we do not loose any rank information. + // This is essentially a custom `self.merge` strategy + self.types_store.dangerousSetVarRedirect(vars.a.var_, fresh_alias_var) catch return Error.AllocatorError; + self.types_store.dangerousSetVarRedirect(vars.b.var_, fresh_alias_var) catch return Error.AllocatorError; + }, + .recursion_var => |_| { + // Unify alias backing var with recursion var + try self.unifyGuarded(backing_var, vars.b.var_); + }, + .err => self.merge(vars, .err), + } + } + + /// Unify two aliases + /// + /// This function assumes the caller has already checked that the alias names match + /// + /// this checks: + /// * that the arities are the same + /// * that parallel arguments unify + /// + /// NOTE: the rust version of this function `unify_two_aliases` is *significantly* more + /// complicated than the version here + fn unifyTwoAliases(self: *Self, vars: *const ResolvedVarDescs, a_alias: Alias, b_alias: Alias) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + if (a_alias.vars.nonempty.count != b_alias.vars.nonempty.count) { + return error.TypeMismatch; + } + + // Unify each pair of arguments + const a_args_slice = self.types_store.sliceAliasArgs(a_alias); + const b_args_slice = self.types_store.sliceAliasArgs(b_alias); + for (a_args_slice, b_args_slice) |a_arg, b_arg| { + try self.unifyGuarded(a_arg, b_arg); + } + + // Rust compiler comment: + // Don't report real_var mismatches, because they must always be surfaced higher, from the argument types. + const a_backing_var = self.types_store.getAliasBackingVar(a_alias); + const b_backing_var = self.types_store.getAliasBackingVar(b_alias); + self.unifyGuarded(a_backing_var, b_backing_var) catch {}; + + // Ensure the target variable has slots for the alias arguments + self.merge(vars, vars.b.desc.content); + } + + // Unify structure // + + /// Unify when `a` is a structure type + fn unifyStructure( + self: *Self, + vars: *const ResolvedVarDescs, + a_flat_type: FlatType, + b_content: Content, + ) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + switch (b_content) { + .flex => |b_flex| { + if (b_flex.constraints.len() > 0) { + // Record that we need to check constraints later + _ = self.scratch.deferred_constraints.append(self.scratch.gpa, DeferredConstraintCheck{ + .var_ = vars.b.var_, // Since the vars are merge, we arbitrary choose b + .constraints = b_flex.constraints, + }) catch return Error.AllocatorError; + } + + self.merge(vars, Content{ .structure = a_flat_type }); + }, + .rigid => return error.TypeMismatch, + .alias => |b_alias| { + // When unifying an alias with a concrete structure, we + // want to preserve the alias for display while ensuring the + // types are compatible. + + const backing_var = self.types_store.getAliasBackingVar(b_alias); + + // First, we unify the concrete var with the alias backing var + // IMPORTANT: The arg order here is important! Unifying + // updates the second var to hold the type, and the first + // var to redirect to the second + try self.unifyGuarded(vars.a.var_, backing_var); + + // Next, we create a fresh alias (which internally points to `backing_var`), + // then we redirect both a & b to the new alias. + const fresh_alias_var = self.fresh(vars, .{ .alias = b_alias }) catch return Error.AllocatorError; + + // These redirects are safe because fresh_alias_var is created at min(a_rank, b_rank). + // Because of this, we do not loose any rank information. + // This is essentially a custom `self.merge` strategy + self.types_store.dangerousSetVarRedirect(vars.a.var_, fresh_alias_var) catch return Error.AllocatorError; + self.types_store.dangerousSetVarRedirect(vars.b.var_, fresh_alias_var) catch return Error.AllocatorError; + }, + .structure => |b_flat_type| { + try self.unifyFlatType(vars, a_flat_type, b_flat_type); + }, + .recursion_var => |b_rec_var| { + // When unifying structure with recursion var, unify with the structure + // the recursion var points to + try self.unifyGuarded(vars.a.var_, b_rec_var.structure); + }, + .err => self.merge(vars, .err), + } + } + + // Unify recursion var // + + /// Unify when `a` is a recursion variable + /// + /// Equirecursive unification: Two recursive types unify if they are structurally + /// equal up to their recursion points. RecursionVar marks these recursion points + /// and prevents infinite expansion during unification. + /// + /// The key insight: when we encounter a RecursionVar, we unify with the structure + /// it points to. The existing cycle detection in unifyGuarded (via checkVarsEquiv) + /// ensures we don't infinitely recurse - if we've already unified these exact vars, + /// we return early. + fn unifyRecursionVar( + self: *Self, + vars: *const ResolvedVarDescs, + a_rec_var: RecursionVar, + b_content: Content, + ) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + switch (b_content) { + .flex => |b_flex| { + // RecursionVar can unify with flex - defer constraints and merge + if (b_flex.constraints.len() > 0) { + // Record that we need to check constraints later + _ = self.scratch.deferred_constraints.append(self.scratch.gpa, DeferredConstraintCheck{ + .var_ = vars.b.var_, + .constraints = b_flex.constraints, + }) catch return Error.AllocatorError; + } + self.merge(vars, vars.a.desc.content); + }, + .rigid => { + // RecursionVar cannot unify with rigid - rigid types have no structure to recurse into + return error.TypeMismatch; + }, + .alias => |b_alias| { + // Unify with the alias backing var to preserve the alias structure + // This allows RecursionVar to work through type aliases + const backing_var = self.types_store.getAliasBackingVar(b_alias); + try self.unifyGuarded(vars.a.var_, backing_var); + }, + .structure => { + // Unify the structure the recursion var points to with b's structure + // This is equirecursive unification: unfold one level and continue + try self.unifyGuarded(a_rec_var.structure, vars.b.var_); + }, + .recursion_var => |b_rec_var| { + // Both are recursion vars - the heart of equirecursive unification + // We unify their structures. If they form a cycle, checkVarsEquiv + // in unifyGuarded will detect it and prevent infinite recursion. + try self.unifyGuarded(a_rec_var.structure, b_rec_var.structure); + }, + .err => self.merge(vars, .err), + } + } + + /// Unify when `a` is a structure type + fn unifyFlatType( + self: *Self, + vars: *const ResolvedVarDescs, + a_flat_type: FlatType, + b_flat_type: FlatType, + ) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + switch (a_flat_type) { + .tuple => |a_tuple| { + switch (b_flat_type) { + .tuple => |b_tuple| { + try self.unifyTuple(vars, a_tuple, b_tuple); + }, + else => return error.TypeMismatch, + } + }, + .nominal_type => |a_type| { + const a_backing_var = self.types_store.getNominalBackingVar(a_type); + const a_backing_resolved = self.types_store.resolveVar(a_backing_var); + if (a_backing_resolved.desc.content == .err) { + self.merge(vars, vars.b.desc.content); + return; + } + + switch (b_flat_type) { + .nominal_type => |b_type| { + const b_backing_var = self.types_store.getNominalBackingVar(b_type); + const b_backing_resolved = self.types_store.resolveVar(b_backing_var); + if (b_backing_resolved.desc.content == .err) { + self.merge(vars, vars.a.desc.content); + return; + } + + try self.unifyNominalType(vars, a_type, b_type); + }, + .tag_union => |b_tag_union| { + // Try to unify nominal tag union (a) with anonymous tag union (b) + try self.unifyTagUnionWithNominal(vars, a_type, a_backing_var, a_backing_resolved, b_tag_union, .a_is_nominal); + }, + .empty_tag_union => { + // If this nominal is opaque and we're not in the origin module, error + if (!a_type.canLiftInner(self.module_env.module_name_idx)) { + return error.TypeMismatch; + } + + if (a_backing_resolved.desc.content == .structure and + a_backing_resolved.desc.content.structure == .empty_tag_union) + { + self.merge(vars, vars.a.desc.content); + } else { + return error.TypeMismatch; + } + }, + else => return error.TypeMismatch, + } + }, + .fn_pure => |a_func| { + switch (b_flat_type) { + .fn_pure => |b_func| { + try self.unifyFunc(vars, a_func, b_func); + self.merge(vars, vars.a.desc.content); + }, + .fn_unbound => |b_func| { + // pure unifies with unbound -> pure + try self.unifyFunc(vars, a_func, b_func); + self.merge(vars, vars.a.desc.content); + }, + .fn_effectful => { + // pure cannot unify with effectful + return error.TypeMismatch; + }, + else => return error.TypeMismatch, + } + }, + .fn_effectful => |a_func| { + switch (b_flat_type) { + .fn_effectful => |b_func| { + try self.unifyFunc(vars, a_func, b_func); + self.merge(vars, vars.a.desc.content); + }, + .fn_unbound => |b_func| { + // effectful unifies with unbound -> effectful + try self.unifyFunc(vars, a_func, b_func); + self.merge(vars, vars.a.desc.content); + }, + .fn_pure => { + // effectful cannot unify with pure + return error.TypeMismatch; + }, + else => return error.TypeMismatch, + } + }, + .fn_unbound => |a_func| { + switch (b_flat_type) { + .fn_pure => |b_func| { + // unbound unifies with pure -> pure + try self.unifyFunc(vars, a_func, b_func); + self.merge(vars, vars.b.desc.content); + }, + .fn_effectful => |b_func| { + // unbound unifies with effectful -> effectful + try self.unifyFunc(vars, a_func, b_func); + self.merge(vars, vars.b.desc.content); + }, + .fn_unbound => |b_func| { + // unbound unifies with unbound -> unbound + try self.unifyFunc(vars, a_func, b_func); + self.merge(vars, vars.a.desc.content); + }, + else => return error.TypeMismatch, + } + }, + .record => |a_record| { + switch (b_flat_type) { + .empty_record => { + if (a_record.fields.len() == 0) { + try self.unifyGuarded(a_record.ext, vars.b.var_); + } else { + return error.TypeMismatch; + } + }, + .record => |b_record| { + try self.unifyTwoRecords( + vars, + a_record.fields, + .{ .ext = a_record.ext }, + b_record.fields, + .{ .ext = b_record.ext }, + ); + }, + .record_unbound => |b_fields| { + try self.unifyTwoRecords( + vars, + a_record.fields, + .{ .ext = a_record.ext }, + b_fields, + .unbound, + ); + }, + else => return error.TypeMismatch, + } + }, + .record_unbound => |a_fields| { + switch (b_flat_type) { + .empty_record => { + if (a_fields.len() == 0) { + // Both are empty, merge as empty_record + self.merge(vars, Content{ .structure = .empty_record }); + } else { + return error.TypeMismatch; + } + }, + .record => |b_record| { + try self.unifyTwoRecords( + vars, + a_fields, + .unbound, + b_record.fields, + .{ .ext = b_record.ext }, + ); + }, + .record_unbound => |b_fields| { + try self.unifyTwoRecords( + vars, + a_fields, + .unbound, + b_fields, + .unbound, + ); + }, + else => return error.TypeMismatch, + } + }, + .empty_record => { + switch (b_flat_type) { + .empty_record => { + self.merge(vars, Content{ .structure = .empty_record }); + }, + .record => |b_record| { + if (b_record.fields.len() == 0) { + try self.unifyGuarded(vars.a.var_, b_record.ext); + } else { + return error.TypeMismatch; + } + }, + .record_unbound => |b_fields| { + if (b_fields.len() == 0) { + // Both are empty, merge as empty_record + self.merge(vars, Content{ .structure = .empty_record }); + } else { + return error.TypeMismatch; + } + }, + else => return error.TypeMismatch, + } + }, + .tag_union => |a_tag_union| { + switch (b_flat_type) { + .empty_tag_union => { + if (a_tag_union.tags.len() == 0) { + try self.unifyGuarded(a_tag_union.ext, vars.b.var_); + } else { + return error.TypeMismatch; + } + }, + .tag_union => |b_tag_union| { + try self.unifyTwoTagUnions(vars, a_tag_union, b_tag_union); + }, + .nominal_type => |b_type| { + // If this nominal is opaque and we're not in the origin module, error + if (!b_type.canLiftInner(self.module_env.module_name_idx)) { + return error.TypeMismatch; + } + + // Try to unify anonymous tag union (a) with nominal tag union (b) + const b_backing_var = self.types_store.getNominalBackingVar(b_type); + const b_backing_resolved = self.types_store.resolveVar(b_backing_var); + if (b_backing_resolved.desc.content == .err) { + self.merge(vars, vars.a.desc.content); + return; + } + try self.unifyTagUnionWithNominal(vars, b_type, b_backing_var, b_backing_resolved, a_tag_union, .b_is_nominal); + }, + else => return error.TypeMismatch, + } + }, + .empty_tag_union => { + switch (b_flat_type) { + .empty_tag_union => { + self.merge(vars, Content{ .structure = .empty_tag_union }); + }, + .tag_union => |b_tag_union| { + if (b_tag_union.tags.len() == 0) { + try self.unifyGuarded(vars.a.var_, b_tag_union.ext); + } else { + return error.TypeMismatch; + } + }, + .nominal_type => |b_type| { + // Try to unify empty tag union (a) with nominal tag union (b) + const b_backing_var = self.types_store.getNominalBackingVar(b_type); + const b_backing_resolved = self.types_store.resolveVar(b_backing_var); + if (b_backing_resolved.desc.content == .err) { + self.merge(vars, vars.a.desc.content); + return; + } + + // Check if the nominal's backing is also an empty tag union + if (b_backing_resolved.desc.content == .structure and + b_backing_resolved.desc.content.structure == .empty_tag_union) + { + // Both are empty, unify with the nominal + self.merge(vars, vars.b.desc.content); + } else { + // Nominal has a non-empty backing, can't unify + return error.TypeMismatch; + } + }, + else => return error.TypeMismatch, + } + }, + } + } + + /// unify tuples + /// + /// this checks: + /// * that the arities are the same + /// * that parallel arguments unify + fn unifyTuple( + self: *Self, + vars: *const ResolvedVarDescs, + a_tuple: Tuple, + b_tuple: Tuple, + ) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + if (a_tuple.elems.len() != b_tuple.elems.len()) { + return error.TypeMismatch; + } + + const a_elems = self.types_store.sliceVars(a_tuple.elems); + const b_elems = self.types_store.sliceVars(b_tuple.elems); + for (a_elems, b_elems) |a_elem, b_elem| { + try self.unifyGuarded(a_elem, b_elem); + } + + self.merge(vars, vars.b.desc.content); + } + + // Unify nominal type // + + /// Unify when `a` was a nominal type + fn unifyNominalType(self: *Self, vars: *const ResolvedVarDescs, a_type: NominalType, b_type: NominalType) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + // Check if either nominal type has an invalid backing variable + const a_backing_var = self.types_store.getNominalBackingVar(a_type); + const a_backing_resolved = self.types_store.resolveVar(a_backing_var); + if (a_backing_resolved.desc.content == .err) { + // Invalid nominal type - treat as transparent + self.merge(vars, vars.b.desc.content); + return; + } + + const b_backing_var = self.types_store.getNominalBackingVar(b_type); + const b_backing_resolved = self.types_store.resolveVar(b_backing_var); + if (b_backing_resolved.desc.content == .err) { + // Invalid nominal type - treat as transparent + self.merge(vars, vars.a.desc.content); + return; + } + + if (!TypeIdent.eql(self.module_env.getIdentStore(), a_type.ident, b_type.ident)) { + return error.TypeMismatch; + } + + if (a_type.vars.nonempty.count != b_type.vars.nonempty.count) { + return error.TypeMismatch; + } + + // Unify each pair of arguments using iterators + const a_slice = self.types_store.sliceNominalArgs(a_type); + const b_slice = self.types_store.sliceNominalArgs(b_type); + for (a_slice, b_slice) |a_arg, b_arg| { + try self.unifyGuarded(a_arg, b_arg); + } + + // Note that we *do not* unify backing variable + + self.merge(vars, vars.b.desc.content); + } + + fn unifyTagUnionWithNominal( + self: *Self, + vars: *const ResolvedVarDescs, + nominal_type: NominalType, + nominal_backing_var: Var, + nominal_backing_resolved: ResolvedVarDesc, + anon_tag_union: TagUnion, + direction: NominalDirection, + ) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + // If this nominal is opaque and we're not in the origin module, error + if (!nominal_type.canLiftInner(self.module_env.module_name_idx)) { + return error.TypeMismatch; + } + + // Check if the nominal's backing type is a tag union (including empty) + const nominal_backing_content = nominal_backing_resolved.desc.content; + if (nominal_backing_content != .structure) { + return error.TypeMismatch; + } + + const nominal_backing_flat = nominal_backing_content.structure; + + // Handle empty tag union case + if (nominal_backing_flat == .empty_tag_union) { + // The nominal's backing is an empty tag union [] + // The anon_tag_union should also be empty for unification to succeed + if (anon_tag_union.tags.len() == 0) { + // Both are empty - unify the extension variables + try self.unifyGuarded(anon_tag_union.ext, nominal_backing_var); + + // Merge to the NOMINAL type (not the tag union) + switch (direction) { + .a_is_nominal => self.merge(vars, vars.a.desc.content), + .b_is_nominal => self.merge(vars, vars.b.desc.content), + } + return; + } else { + // Anon has tags but nominal is empty + return error.TypeMismatch; + } + } + + if (nominal_backing_flat != .tag_union) { + // Nominal's backing is not a tag union (could be record, tuple, etc.) + // Cannot unify anonymous tag union with non-tag-union nominal + return error.TypeMismatch; + } + + const nominal_backing_tag_union = nominal_backing_flat.tag_union; + + // Unify the two tag unions directly (without modifying the nominal's backing) + // This checks that: + // - All tags in the anonymous union exist in the nominal union + // - Payload types match + // - Extension variables are compatible + try self.unifyTwoTagUnions(vars, anon_tag_union, nominal_backing_tag_union); + + // If we get here, unification succeeded! + // Merge to the NOMINAL type (not the tag union) + // This is the key: the nominal type "wins" + switch (direction) { + .a_is_nominal => { + // Merge to a (which is the nominal) + self.merge(vars, vars.a.desc.content); + }, + .b_is_nominal => { + // Merge to b (which is the nominal) + self.merge(vars, vars.b.desc.content); + }, + } + } + + /// unify func + /// + /// this checks: + /// * that the arg arities are the same + /// * that parallel args unify + /// * that ret unifies + fn unifyFunc( + self: *Self, + _: *const ResolvedVarDescs, + a_func: Func, + b_func: Func, + ) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + if (a_func.args.len() != b_func.args.len()) { + return error.TypeMismatch; + } + + const a_args = self.types_store.sliceVars(a_func.args); + const b_args = self.types_store.sliceVars(b_func.args); + for (a_args, b_args) |a_arg, b_arg| { + try self.unifyGuarded(a_arg, b_arg); + } + + try self.unifyGuarded(a_func.ret, b_func.ret); + } + + /// Unify two extensible records. + /// + /// This function implements Elm-style record unification. + /// + /// Each record consists of: + /// - a fixed set of known fields (`fields`) + /// - an extensible tail variable (`ext`) that may point to additional unknown fields + /// + /// Given two records `a` and `b`, we: + /// 1. Collect all known fields by unwrapping their `ext` chains. + /// 2. Partition the field sets into: + /// - `in_both`: shared fields present in both `a` and `b` + /// - `only_in_a`: fields only present in `a` + /// - `only_in_b`: fields only present in `b` + /// 3. Determine the relationship between the two records based on these partitions. + /// + /// Four cases follow: + /// + /// --- + /// + /// **Case 1: Exactly the Same Fields** + /// + /// a = { x, y, ..others_a } + /// b = { x, y, ..others_b } + /// + /// - All fields are shared + /// - We unify `others_a ~ others_b` + /// - Then unify each shared field pair + /// + /// --- + /// + /// **Case 2: `a` Extends `b`** + /// + /// a = { x, y, z, ..others_a } + /// b = { x, y, ..others_b } + /// + /// - `a` has additional fields not in `b` + /// - We generate a new var `only_in_a_var = { z, ..others_a }` + /// - Unify `only_in_a_var ~ others_b` + /// - Then unify shared fields + /// + /// --- + /// + /// **Case 3: `b` Extends `a`** + /// + /// a = { x, y, ..others_a } + /// b = { x, y, z, ..others_b } + /// + /// - Same as Case 2, but reversed + /// - `b` has additional fields not in `a` + /// - We generate a new var `only_in_b_var = { z, ..others_b }` + /// - Unify `others_a ~ only_in_b_var` + /// - Then unify shared fields + /// + /// --- + /// + /// **Case 4: Both Extend Each Other** + /// + /// a = { x, y, z, ..others_a } + /// b = { x, y, w, ..others_b } + /// + /// - Each has unique fields the other lacks + /// - Generate: + /// - shared_others = fresh flex_var + /// - only_in_a_var = { z, ..shared_others } + /// - only_in_b_var = { w, ..shared_others } + /// - Unify: + /// - `others_a ~ only_in_b_var` + /// - `only_in_a_var ~ others_b` + /// - Then unify shared fields into `{ x, y, ..shared_others }` + /// + /// --- + /// + /// All field unification is done using `unifySharedFields`, and new variables are created using `fresh`. + /// + /// This function does not attempt to deduplicate fields or reorder them — callers are responsible + /// for providing consistent field names. + fn unifyTwoRecords( + self: *Self, + vars: *const ResolvedVarDescs, + a_fields: RecordField.SafeMultiList.Range, + a_ext: RecordExt, + b_fields: RecordField.SafeMultiList.Range, + b_ext: RecordExt, + ) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + // First, unwrap all fields for record, erroring if we encounter an + // invalid record ext var + const a_gathered_fields = try self.gatherRecordFields(a_fields, a_ext); + const b_gathered_fields = try self.gatherRecordFields(b_fields, b_ext); + + // Then partition the fields + const partitioned = Self.partitionFields( + self.module_env.getIdentStore(), + self.scratch, + a_gathered_fields.range, + b_gathered_fields.range, + ) catch return Error.AllocatorError; + + // Determine how the fields of a & b extend + const a_has_uniq_fields = partitioned.only_in_a.len() > 0; + const b_has_uniq_fields = partitioned.only_in_b.len() > 0; + + var fields_ext: FieldsExtension = .exactly_the_same; + if (a_has_uniq_fields and b_has_uniq_fields) { + fields_ext = .both_extend; + } else if (a_has_uniq_fields) { + fields_ext = .a_extends_b; + } else if (b_has_uniq_fields) { + fields_ext = .b_extends_a; + } + + const a_gathered_ext = blk: { + switch (a_gathered_fields.ext) { + .unbound => break :blk self.fresh(vars, .{ .flex = Flex.init() }) catch return Error.AllocatorError, + .ext => |ext_var| break :blk ext_var, + } + }; + const b_gathered_ext = blk: { + switch (b_gathered_fields.ext) { + .unbound => break :blk self.fresh(vars, .{ .flex = Flex.init() }) catch return Error.AllocatorError, + .ext => |ext_var| break :blk ext_var, + } + }; + + // Unify fields + switch (fields_ext) { + .exactly_the_same => { + // Unify exts + try self.unifyGuarded(a_gathered_ext, b_gathered_ext); + + // Unify shared fields + // This copies fields from scratch into type_store + try self.unifySharedFields( + vars, + self.scratch.in_both_fields.sliceRange(partitioned.in_both), + null, + null, + a_gathered_ext, + ); + }, + .a_extends_b => { + // Create a new variable of a record with only a's uniq fields + // This copies fields from scratch into type_store + const only_in_a_fields_range = self.types_store.appendRecordFields( + self.scratch.only_in_a_fields.sliceRange(partitioned.only_in_a), + ) catch return Error.AllocatorError; + const only_in_a_var = self.fresh(vars, Content{ .structure = FlatType{ .record = .{ + .fields = only_in_a_fields_range, + .ext = a_gathered_ext, + } } }) catch return Error.AllocatorError; + + // Unify the sub record with b's ext + try self.unifyGuarded(only_in_a_var, b_gathered_ext); + + // Unify shared fields + // This copies fields from scratch into type_store + try self.unifySharedFields( + vars, + self.scratch.in_both_fields.sliceRange(partitioned.in_both), + null, + null, + only_in_a_var, + ); + }, + .b_extends_a => { + // Create a new variable of a record with only b's uniq fields + // This copies fields from scratch into type_store + const only_in_b_fields_range = self.types_store.appendRecordFields( + self.scratch.only_in_b_fields.sliceRange(partitioned.only_in_b), + ) catch return Error.AllocatorError; + const only_in_b_var = self.fresh(vars, Content{ .structure = FlatType{ .record = .{ + .fields = only_in_b_fields_range, + .ext = b_gathered_ext, + } } }) catch return Error.AllocatorError; + + // Unify the sub record with a's ext + try self.unifyGuarded(a_gathered_ext, only_in_b_var); + + // Unify shared fields + // This copies fields from scratch into type_store + try self.unifySharedFields( + vars, + self.scratch.in_both_fields.sliceRange(partitioned.in_both), + null, + null, + only_in_b_var, + ); + }, + .both_extend => { + // Create a new variable of a record with only a's uniq fields + // This copies fields from scratch into type_store + const only_in_a_fields_range = self.types_store.appendRecordFields( + self.scratch.only_in_a_fields.sliceRange(partitioned.only_in_a), + ) catch return Error.AllocatorError; + const only_in_a_var = self.fresh(vars, Content{ .structure = FlatType{ .record = .{ + .fields = only_in_a_fields_range, + .ext = a_gathered_ext, + } } }) catch return Error.AllocatorError; + + // Create a new variable of a record with only b's uniq fields + // This copies fields from scratch into type_store + const only_in_b_fields_range = self.types_store.appendRecordFields( + self.scratch.only_in_b_fields.sliceRange(partitioned.only_in_b), + ) catch return Error.AllocatorError; + const only_in_b_var = self.fresh(vars, Content{ .structure = FlatType{ .record = .{ + .fields = only_in_b_fields_range, + .ext = b_gathered_ext, + } } }) catch return Error.AllocatorError; + + // Create a new ext var + const new_ext_var = self.fresh(vars, .{ .flex = Flex.init() }) catch return Error.AllocatorError; + + // Unify the sub records with exts + try self.unifyGuarded(a_gathered_ext, only_in_b_var); + try self.unifyGuarded(only_in_a_var, b_gathered_ext); + + // Unify shared fields + // This copies fields from scratch into type_store + try self.unifySharedFields( + vars, + self.scratch.in_both_fields.sliceRange(partitioned.in_both), + self.scratch.only_in_a_fields.sliceRange(partitioned.only_in_a), + self.scratch.only_in_b_fields.sliceRange(partitioned.only_in_b), + new_ext_var, + ); + }, + } + } + + const FieldsExtension = enum { exactly_the_same, a_extends_b, b_extends_a, both_extend }; + + const RecordExt = union(enum) { ext: Var, unbound }; + + const GatheredFields = struct { ext: RecordExt, range: RecordFieldSafeList.Range }; + + /// Recursively unwraps the fields of an extensible record, flattening all visible fields + /// into `scratch.gathered_fields` and following through: + /// * aliases (by chasing `.backing_var`) + /// * record extension chains (via nested `.record.ext`) + /// + /// Returns: + /// * a `Range` indicating the location of the gathered fields in `gathered_fields` + /// * the final tail extension variable, which is either a flex var or an empty record + /// + /// Errors if it encounters a malformed or invalid extension (e.g. a non-record type). + fn gatherRecordFields(self: *Self, record_fields: RecordField.SafeMultiList.Range, record_ext: RecordExt) Error!GatheredFields { + // first, copy from the store's MultiList record fields array into scratch's + // regular list, capturing the insertion range + var range = self.scratch.copyGatherFieldsFromMultiList( + &self.types_store.record_fields, + record_fields, + ) catch return Error.AllocatorError; + + // Note: If a field name appears multiple times (e.g., in both base and extension), + // we keep the leftmost field (left-bias semantics, like Haskell's Map.union). + // Duplicates within a single record's extension chain are rare and typically + // indicate a type system bug (e.g., malformed type like `{ name: Str, ..{ name: U32 } }`). + // The outer unification logic handles unifying fields across *different* records. + + // Recursively gather fields from extensions + var ext = record_ext; + var guard = types_mod.debug.IterationGuard.init("gatherRecordFields"); + while (true) { + guard.tick(); + switch (ext) { + .unbound => { + return .{ .ext = ext, .range = range }; + }, + .ext => |ext_var| { + switch (self.types_store.resolveVar(ext_var).desc.content) { + .flex => { + return .{ .ext = .{ .ext = ext_var }, .range = range }; + }, + .rigid => { + return .{ .ext = .{ .ext = ext_var }, .range = range }; + }, + .alias => |alias| { + ext = .{ .ext = self.types_store.getAliasBackingVar(alias) }; + }, + .structure => |flat_type| { + switch (flat_type) { + .record => |ext_record| { + const next_fields = self.types_store.record_fields.sliceRange(ext_record.fields); + + // Merge extension fields while maintaining sorted order + self.scratch.mergeSortedExtensionFields( + &range, + next_fields.items(.name), + next_fields.items(.var_), + self.module_env.getIdentStore(), + ) catch return Error.AllocatorError; + + ext = .{ .ext = ext_record.ext }; + }, + .record_unbound => |fields| { + const next_fields = self.types_store.record_fields.sliceRange(fields); + + // Merge extension fields while maintaining sorted order + self.scratch.mergeSortedExtensionFields( + &range, + next_fields.items(.name), + next_fields.items(.var_), + self.module_env.getIdentStore(), + ) catch return Error.AllocatorError; + + return .{ .ext = ext, .range = range }; + }, + .empty_record => { + return .{ .ext = ext, .range = range }; + }, + else => try self.setUnifyErrAndThrow(.{ .invalid_record_ext = ext_var }), + } + }, + else => try self.setUnifyErrAndThrow(.{ .invalid_record_ext = ext_var }), + } + }, + } + } + } + + const PartitionedRecordFields = struct { + only_in_a: RecordFieldSafeList.Range, + only_in_b: RecordFieldSafeList.Range, + in_both: TwoRecordFieldsSafeList.Range, + }; + + /// Given two ranges of record fields stored in `scratch.gathered_fields`, this function: + /// * sorts both slices in-place by field name + /// * partitions them into three disjoint groups: + /// - fields only in `a` + /// - fields only in `b` + /// - fields present in both (by name) + /// + /// These groups are stored into dedicated scratch buffers: + /// * `only_in_a_fields` + /// * `only_in_b_fields` + /// * `in_both_fields` + /// + /// The result is a set of ranges that can be used to slice those buffers. + /// + /// The caller must not mutate the field ranges between `gatherRecordFields` and `partitionFields`. + fn partitionFields( + ident_store: *const Ident.Store, + scratch: *Scratch, + a_fields_range: RecordFieldSafeList.Range, + b_fields_range: RecordFieldSafeList.Range, + ) std.mem.Allocator.Error!PartitionedRecordFields { + // Sort the fields (gathering maintains partial order, but unification may create unsorted unions) + const a_fields = scratch.gathered_fields.sliceRange(a_fields_range); + std.mem.sort(RecordField, a_fields, ident_store, comptime RecordField.sortByNameAsc); + const b_fields = scratch.gathered_fields.sliceRange(b_fields_range); + std.mem.sort(RecordField, b_fields, ident_store, comptime RecordField.sortByNameAsc); + + // Get the start of index of the new range + const a_fields_start: u32 = @intCast(scratch.only_in_a_fields.len()); + const b_fields_start: u32 = @intCast(scratch.only_in_b_fields.len()); + const both_fields_start: u32 = @intCast(scratch.in_both_fields.len()); + + // Iterate over the fields in order, grouping them + var a_i: usize = 0; + var b_i: usize = 0; + while (a_i < a_fields.len and b_i < b_fields.len) { + const a_next = a_fields[a_i]; + const b_next = b_fields[b_i]; + const ord = RecordField.orderByName(ident_store, a_next, b_next); + switch (ord) { + .eq => { + _ = try scratch.in_both_fields.append(scratch.gpa, TwoRecordFields{ + .a = a_next, + .b = b_next, + }); + a_i = a_i + 1; + b_i = b_i + 1; + }, + .lt => { + _ = try scratch.only_in_a_fields.append(scratch.gpa, a_next); + a_i = a_i + 1; + }, + .gt => { + _ = try scratch.only_in_b_fields.append(scratch.gpa, b_next); + b_i = b_i + 1; + }, + } + } + + // If b was shorter, add the extra a elems + while (a_i < a_fields.len) { + const a_next = a_fields[a_i]; + _ = try scratch.only_in_a_fields.append(scratch.gpa, a_next); + a_i = a_i + 1; + } + + // If a was shorter, add the extra b elems + while (b_i < b_fields.len) { + const b_next = b_fields[b_i]; + _ = try scratch.only_in_b_fields.append(scratch.gpa, b_next); + b_i = b_i + 1; + } + + // Return the ranges + return .{ + .only_in_a = scratch.only_in_a_fields.rangeToEnd(a_fields_start), + .only_in_b = scratch.only_in_b_fields.rangeToEnd(b_fields_start), + .in_both = scratch.in_both_fields.rangeToEnd(both_fields_start), + }; + } + + /// Given a list of shared fields & a list of extended fields, unify the shared + /// Then merge a new record with both shared+extended fields + fn unifySharedFields( + self: *Self, + vars: *const ResolvedVarDescs, + shared_fields: TwoRecordFieldsSafeList.Slice, + mb_a_extended_fields: ?RecordFieldSafeList.Slice, + mb_b_extended_fields: ?RecordFieldSafeList.Slice, + ext: Var, + ) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + // First, unify all field types. This may cause nested record unifications + // which will append their own fields to the store. We must NOT interleave + // our field appends with these nested calls. + for (shared_fields) |shared| { + try self.unifyGuarded(shared.a.var_, shared.b.var_); + } + + // Now that all nested unifications are complete, append OUR fields. + // This ensures our fields form a contiguous range. + const range_start: u32 = self.types_store.record_fields.len(); + + for (shared_fields) |shared| { + _ = self.types_store.appendRecordFields(&[_]RecordField{.{ + .name = shared.b.name, + .var_ = shared.b.var_, + }}) catch return Error.AllocatorError; + } + + // Append combined fields + if (mb_a_extended_fields) |extended_fields| { + _ = self.types_store.appendRecordFields(extended_fields) catch return Error.AllocatorError; + } + if (mb_b_extended_fields) |extended_fields| { + _ = self.types_store.appendRecordFields(extended_fields) catch return Error.AllocatorError; + } + + // Merge vars - now the range correctly contains only THIS record's fields + self.merge(vars, Content{ .structure = FlatType{ .record = .{ + .fields = self.types_store.record_fields.rangeToEnd(range_start), + .ext = ext, + } } }); + } + + /// Unify two extensible tag union. + /// + /// This function implements Elm-style record unification, but for tag unions. + /// + /// Each tag union consists of: + /// - a fixed set of known tags (`tags`) + /// - an extensible tail variable (`ext`) that may point to additional unknown tags + /// + /// Given two tag unions `a` and `b`, we: + /// 1. Collect all known tags by unwrapping their `ext` chains. + /// 2. Partition the tags sets into: + /// - `in_both`: shared fields present in both `a` and `b` + /// - `only_in_a`: fields only present in `a` + /// - `only_in_b`: fields only present in `b` + /// 3. Determine the relationship between the two tag unions based on these partitions. + /// + /// Four cases follow: + /// + /// --- + /// + /// **Case 1: Exactly the Same Tags** + /// + /// a = [ X ]ext_a + /// b = [ X ]ext_b + /// + /// - All tags are shared + /// - We unify `ext_a ~ ext_b` + /// - Then unify each shared tag pair + /// + /// --- + /// + /// **Case 2: `a` Extends `b`** + /// + /// a = [ X, Y, Z ]ext_a + /// b = [ X, Y ]ext_b + /// + /// - `a` has additional tags not in `b` + /// - We generate a new var `only_in_a_var = [ Z ]ext_a` + /// - Unify `only_in_a_var ~ ext_b` + /// - Then unify shared tags into `[ X, Y ]only_in_a_var` + /// + /// --- + /// + /// **Case 3: `b` Extends `a`** + /// + /// a = [ X, Y ]ext_a + /// b = [ X, Y, Z ]ext_b + /// + /// - Same as Case 2, but reversed + /// - `b` has additional tags not in `a` + /// - We generate a new var `only_in_b_var = [ Z ]ext_b` + /// - Unify `ext_a ~ only_in_b_var` + /// - Then unify shared tags into `[ X, Y ]only_in_b_var` + /// + /// --- + /// + /// **Case 4: Both Extend Each Other** + /// + /// a = [ X, Y, Z ]ext_a + /// b = [ X, Y, W ]ext_b + /// + /// - Each has unique tags the other lacks + /// - Generate: + /// - shared_ext = fresh flex_var + /// - only_in_a_var = [ Z ]shared_ext + /// - only_in_b_var = [ W ]shared_ext + /// - Unify: + /// - `ext_a ~ only_in_b_var` + /// - `only_in_a_var ~ ext_b` + /// - Then unify shared tags into `[ X, Y ]shared_ext` + /// + /// --- + /// + /// All tag unification is done using `unifySharedTags`, and new variables are created using `fresh`. + /// + /// This function does not attempt to deduplicate tags or reorder them — callers are responsible + /// for providing consistent tag names. + fn unifyTwoTagUnions( + self: *Self, + vars: *const ResolvedVarDescs, + a_tag_union: TagUnion, + b_tag_union: TagUnion, + ) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + // First, unwrap all fields for tag unions, erroring if we encounter an + // invalid record ext var + const a_gathered_tags = try self.gatherTagUnionTags(a_tag_union); + const b_gathered_tags = try self.gatherTagUnionTags(b_tag_union); + + // Then partition the tags + const partitioned = Self.partitionTags( + self.module_env.getIdentStore(), + self.scratch, + a_gathered_tags.range, + b_gathered_tags.range, + ) catch return Error.AllocatorError; + + // Determine how the tags of a & b extend + const a_has_uniq_tags = partitioned.only_in_a.len() > 0; + const b_has_uniq_tags = partitioned.only_in_b.len() > 0; + + var tags_ext: TagsExtension = .exactly_the_same; + if (a_has_uniq_tags and b_has_uniq_tags) { + tags_ext = .both_extend; + } else if (a_has_uniq_tags) { + tags_ext = .a_extends_b; + } else if (b_has_uniq_tags) { + tags_ext = .b_extends_a; + } + + // Unify tags + switch (tags_ext) { + .exactly_the_same => { + // Unify exts + try self.unifyGuarded(a_gathered_tags.ext, b_gathered_tags.ext); + + // Unify shared tags + // This copies tags from scratch into type_store + try self.unifySharedTags( + vars, + self.scratch.in_both_tags.sliceRange(partitioned.in_both), + null, + null, + a_gathered_tags.ext, + ); + }, + .a_extends_b => { + // Create a new variable of a tag_union with only a's uniq tags + // This copies tags from scratch into type_store + const only_in_a_tags_range = self.types_store.appendTags( + self.scratch.only_in_a_tags.sliceRange(partitioned.only_in_a), + ) catch return Error.AllocatorError; + const only_in_a_var = self.fresh(vars, Content{ .structure = FlatType{ .tag_union = .{ + .tags = only_in_a_tags_range, + .ext = a_gathered_tags.ext, + } } }) catch return Error.AllocatorError; + + // Unify the sub tag_union with b's ext + try self.unifyGuarded(only_in_a_var, b_gathered_tags.ext); + + // Unify shared tags + // This copies tags from scratch into type_store + try self.unifySharedTags( + vars, + self.scratch.in_both_tags.sliceRange(partitioned.in_both), + null, + null, + only_in_a_var, + ); + }, + .b_extends_a => { + // Create a new variable of a tag_union with only b's uniq tags + // This copies tags from scratch into type_store + const only_in_b_tags_range = self.types_store.appendTags( + self.scratch.only_in_b_tags.sliceRange(partitioned.only_in_b), + ) catch return Error.AllocatorError; + const only_in_b_var = self.fresh(vars, Content{ .structure = FlatType{ .tag_union = .{ + .tags = only_in_b_tags_range, + .ext = b_gathered_tags.ext, + } } }) catch return Error.AllocatorError; + + // Unify the sub tag_union with a's ext + try self.unifyGuarded(a_gathered_tags.ext, only_in_b_var); + + // Unify shared tags + // This copies tags from scratch into type_store + try self.unifySharedTags( + vars, + self.scratch.in_both_tags.sliceRange(partitioned.in_both), + null, + null, + only_in_b_var, + ); + }, + .both_extend => { + // Create a shared extension variable first + // This is critical: both only_in_a_var and only_in_b_var must use this + // shared extension to avoid creating circular type references + const new_ext_var = self.fresh(vars, .{ .flex = Flex.init() }) catch return Error.AllocatorError; + + // Create a new variable of a tag_union with only a's uniq tags + // Uses new_ext_var (not a_gathered_tags.ext) to prevent cycles + const only_in_a_tags_range = self.types_store.appendTags( + self.scratch.only_in_a_tags.sliceRange(partitioned.only_in_a), + ) catch return Error.AllocatorError; + const only_in_a_var = self.fresh(vars, Content{ .structure = FlatType{ .tag_union = .{ + .tags = only_in_a_tags_range, + .ext = new_ext_var, + } } }) catch return Error.AllocatorError; + + // Create a new variable of a tag_union with only b's uniq tags + // Uses new_ext_var (not b_gathered_tags.ext) to prevent cycles + const only_in_b_tags_range = self.types_store.appendTags( + self.scratch.only_in_b_tags.sliceRange(partitioned.only_in_b), + ) catch return Error.AllocatorError; + const only_in_b_var = self.fresh(vars, Content{ .structure = FlatType{ .tag_union = .{ + .tags = only_in_b_tags_range, + .ext = new_ext_var, + } } }) catch return Error.AllocatorError; + + // Unify the sub tag_unions with exts + try self.unifyGuarded(a_gathered_tags.ext, only_in_b_var); + try self.unifyGuarded(only_in_a_var, b_gathered_tags.ext); + + // Unify shared tags + // Include all tags in the merged type - both shared and unique + // The unique tags must be included because the merged type is what + // callers see, and the extension chain via only_in_a_var/only_in_b_var + // is for proper type equality, not for visibility + try self.unifySharedTags( + vars, + self.scratch.in_both_tags.sliceRange(partitioned.in_both), + self.scratch.only_in_a_tags.sliceRange(partitioned.only_in_a), + self.scratch.only_in_b_tags.sliceRange(partitioned.only_in_b), + new_ext_var, + ); + }, + } + } + + const TagsExtension = enum { exactly_the_same, a_extends_b, b_extends_a, both_extend }; + + const GatheredTags = struct { ext: Var, range: TagSafeList.Range }; + + /// Recursively unwraps the tags of an extensible tag_union, flattening all visible tags + /// into `scratch.gathered_tags` and following through: + /// * aliases (by chasing `.backing_var`) + /// * tag_union extension chains (via nested `.tag_union.ext`) + /// + /// Returns: + /// * a `Range` indicating the location of the gathered tags in `gathered_tags` + /// * the final tail extension variable, which is either a flex var or an empty tag_union + /// + /// Errors if it encounters a malformed or invalid extension (e.g. a non-tag_union type). + fn gatherTagUnionTags(self: *Self, tag_union: TagUnion) Error!GatheredTags { + // first, copy from the store's MultiList record fields array into scratch's + // regular list, capturing the insertion range + var range = self.scratch.copyGatherTagsFromMultiList( + &self.types_store.tags, + tag_union.tags, + ) catch return Error.AllocatorError; + + // then loop gathering extensible tags + var ext_var = tag_union.ext; + var guard = types_mod.debug.IterationGuard.init("gatherTagUnionTags"); + while (true) { + guard.tick(); + switch (self.types_store.resolveVar(ext_var).desc.content) { + .flex => { + return .{ .ext = ext_var, .range = range }; + }, + .rigid => { + return .{ .ext = ext_var, .range = range }; + }, + .alias => |alias| { + ext_var = self.types_store.getAliasBackingVar(alias); + }, + .structure => |flat_type| { + switch (flat_type) { + .tag_union => |ext_tag_union| { + const next_tags = self.types_store.tags.sliceRange(ext_tag_union.tags); + + // Merge extension tags while maintaining sorted order + self.scratch.mergeSortedExtensionTags( + &range, + next_tags.items(.name), + next_tags.items(.args), + self.module_env.getIdentStore(), + ) catch return Error.AllocatorError; + + ext_var = ext_tag_union.ext; + }, + .empty_tag_union => { + return .{ .ext = ext_var, .range = range }; + }, + else => try self.setUnifyErrAndThrow(.{ .invalid_tag_union_ext = ext_var }), + } + }, + else => try self.setUnifyErrAndThrow(.{ .invalid_tag_union_ext = ext_var }), + } + } + } + + const PartitionedTags = struct { + only_in_a: TagSafeList.Range, + only_in_b: TagSafeList.Range, + in_both: TwoTagsSafeList.Range, + }; + + /// Given two ranges of tag_union tags stored in `scratch.gathered_tags`, this function: + /// * sorts both slices in-place by tag name + /// * partitions them into three disjoint groups: + /// - tags only in `a` + /// - tags only in `b` + /// - tags present in both (by name) + /// + /// These groups are stored into dedicated scratch buffers: + /// * `only_in_a_tags` + /// * `only_in_b_tags` + /// * `in_both_tags` + /// + /// The result is a set of ranges that can be used to slice those buffers. + /// + /// The caller must not mutate the field ranges between `gatherTagUnionTags` and `partitionTags`. + fn partitionTags( + ident_store: *const Ident.Store, + scratch: *Scratch, + a_tags_range: TagSafeList.Range, + b_tags_range: TagSafeList.Range, + ) std.mem.Allocator.Error!PartitionedTags { + // Sort the tags (gathering maintains partial order, but unification may create unsorted unions) + const a_tags = scratch.gathered_tags.sliceRange(a_tags_range); + std.mem.sort(Tag, a_tags, ident_store, comptime Tag.sortByNameAsc); + const b_tags = scratch.gathered_tags.sliceRange(b_tags_range); + std.mem.sort(Tag, b_tags, ident_store, comptime Tag.sortByNameAsc); + + // Get the start of index of the new range + const a_tags_start: u32 = @intCast(scratch.only_in_a_tags.len()); + const b_tags_start: u32 = @intCast(scratch.only_in_b_tags.len()); + const both_tags_start: u32 = @intCast(scratch.in_both_tags.len()); + + // Iterate over the tags in order, grouping them + var a_i: usize = 0; + var b_i: usize = 0; + while (a_i < a_tags.len and b_i < b_tags.len) { + const a_next = a_tags[a_i]; + const b_next = b_tags[b_i]; + const ord = Tag.orderByName(ident_store, a_next, b_next); + switch (ord) { + .eq => { + _ = try scratch.in_both_tags.append(scratch.gpa, TwoTags{ .a = a_next, .b = b_next }); + a_i = a_i + 1; + b_i = b_i + 1; + }, + .lt => { + _ = try scratch.only_in_a_tags.append(scratch.gpa, a_next); + a_i = a_i + 1; + }, + .gt => { + _ = try scratch.only_in_b_tags.append(scratch.gpa, b_next); + b_i = b_i + 1; + }, + } + } + + // If b was shorter, add the extra a elems + while (a_i < a_tags.len) { + const a_next = a_tags[a_i]; + _ = try scratch.only_in_a_tags.append(scratch.gpa, a_next); + a_i = a_i + 1; + } + + // If a was shorter, add the extra b elems + while (b_i < b_tags.len) { + const b_next = b_tags[b_i]; + _ = try scratch.only_in_b_tags.append(scratch.gpa, b_next); + b_i = b_i + 1; + } + + // Return the ranges + return .{ + .only_in_a = scratch.only_in_a_tags.rangeToEnd(a_tags_start), + .only_in_b = scratch.only_in_b_tags.rangeToEnd(b_tags_start), + .in_both = scratch.in_both_tags.rangeToEnd(both_tags_start), + }; + } + + /// Given a list of shared tags & a list of extended tags, unify the shared tags. + /// Then merge a new tag_union with both shared+extended tags + fn unifySharedTags( + self: *Self, + vars: *const ResolvedVarDescs, + shared_tags: []TwoTags, + mb_a_extended_tags: ?[]Tag, + mb_b_extended_tags: ?[]Tag, + ext: Var, + ) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + // IMPORTANT: First unify all shared tag arguments BEFORE recording range_start. + // This is because unifyGuarded may trigger recursive unifications that also + // append tags to the global tags list. We need to record range_start AFTER + // all inner unifications complete, so we only capture this level's tags. + for (shared_tags) |tags| { + const tag_a_args = self.types_store.sliceVars(tags.a.args); + const tag_b_args = self.types_store.sliceVars(tags.b.args); + + if (tag_a_args.len != tag_b_args.len) return error.TypeMismatch; + + for (tag_a_args, tag_b_args) |a_arg, b_arg| { + try self.unifyGuarded(a_arg, b_arg); + } + } + + // NOW record range_start after all inner unifications are done + const range_start: u32 = self.types_store.tags.len(); + + // Append this level's shared tags + for (shared_tags) |tags| { + _ = self.types_store.appendTags(&[_]Tag{.{ + .name = tags.b.name, + .args = tags.b.args, + }}) catch return Error.AllocatorError; + } + + // Append combined tags + if (mb_a_extended_tags) |extended_tags| { + _ = self.types_store.appendTags(extended_tags) catch return Error.AllocatorError; + } + if (mb_b_extended_tags) |extended_tags| { + _ = self.types_store.appendTags(extended_tags) catch return Error.AllocatorError; + } + + // Merge vars (sorting happens in merge() for all tag unions) + self.merge(vars, Content{ .structure = FlatType{ .tag_union = .{ + .tags = self.types_store.tags.rangeToEnd(range_start), + .ext = ext, + } } }); + } + + // constraints // + + fn unifyStaticDispatchConstraints( + self: *Self, + a_constraints: StaticDispatchConstraint.SafeList.Range, + b_constraints: StaticDispatchConstraint.SafeList.Range, + ) Error!StaticDispatchConstraint.SafeList.Range { + const a_len = a_constraints.len(); + const b_len = b_constraints.len(); + + // Early exits for empty ranges + if (a_len == 0 and b_len == 0) { + return .empty(); + } else if (a_len == 0 and b_len > 0) { + return b_constraints; + } else if (a_len > 0 and b_len == 0) { + return a_constraints; + } + + // Partition constraints + const partitioned = self.partitionStaticDispatchConstraints(a_constraints, b_constraints) catch return Error.AllocatorError; + + // Unify shared constraints + if (partitioned.in_both.len() > 0) { + for (self.scratch.in_both_static_dispatch_constraints.sliceRange(partitioned.in_both)) |two_constraints| { + // TODO: Catch type mismatch and throw a custom error message? + try self.unifyStaticDispatchConstraint(two_constraints.a, two_constraints.b); + } + } + + const top: u32 = @intCast(self.types_store.static_dispatch_constraints.len()); + + // Ensure we have enough memory for the new contiguous list + const capacity = partitioned.in_both.len() + partitioned.only_in_a.len() + partitioned.only_in_b.len(); + self.types_store.static_dispatch_constraints.items.ensureUnusedCapacity( + self.types_store.gpa, + capacity, + ) catch return Error.AllocatorError; + + for (self.scratch.in_both_static_dispatch_constraints.sliceRange(partitioned.in_both)) |two_constraints| { + // Here, we append the constraint's b, but since a & b, it doesn't actually matter + self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(two_constraints.b); + } + for (self.scratch.only_in_a_static_dispatch_constraints.sliceRange(partitioned.only_in_a)) |only_a| { + self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(only_a); + } + for (self.scratch.only_in_b_static_dispatch_constraints.sliceRange(partitioned.only_in_b)) |only_b| { + self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(only_b); + } + + return self.types_store.static_dispatch_constraints.rangeToEnd(top); + } + + /// Unify two static dispatch constraints + fn unifyStaticDispatchConstraint( + self: *Self, + a_constraint: StaticDispatchConstraint, + b_constraint: StaticDispatchConstraint, + ) Error!void { + const trace = tracy.trace(@src()); + defer trace.end(); + + // Self-referential constraints like `a.plus : a, a -> a` are valid and expected. + // To prevent infinite recursion when unifying them, we use variable marks to detect + // if we're already in the process of unifying these constraint function variables. + // + // This works together with the occurs check in occurs.zig which follows constraints + // to detect truly infinite types. + const a_desc = self.types_store.resolveVar(a_constraint.fn_var); + const b_desc = self.types_store.resolveVar(b_constraint.fn_var); + + // Check if either variable is marked as "visited" (currently being unified) + if (a_desc.desc.mark == .visited or b_desc.desc.mark == .visited) { + // Already unifying these constraint functions - skip to prevent infinite recursion + return; + } + + // Mark variables as being unified + self.types_store.setDescMark(a_desc.desc_idx, .visited); + self.types_store.setDescMark(b_desc.desc_idx, .visited); + + // Unify the constraint function types + const result = self.unifyGuarded(a_constraint.fn_var, b_constraint.fn_var); + + // Unmark variables + self.types_store.setDescMark(a_desc.desc_idx, .none); + self.types_store.setDescMark(b_desc.desc_idx, .none); + + try result; + } + + const PartitionedStaticDispatchConstraints = struct { + only_in_a: StaticDispatchConstraint.SafeList.Range, + only_in_b: StaticDispatchConstraint.SafeList.Range, + in_both: TwoStaticDispatchConstraints.SafeList.Range, + }; + + /// Given two ranges of record fields stored in `scratch.gathered_fields`, this function: + /// * sorts both slices in-place by field name + /// * partitions them into three disjoint groups: + /// - fields only in `a` + /// - fields only in `b` + /// - fields present in both (by name) + /// + /// These groups are stored into dedicated scratch buffers: + /// * `only_in_a_fields` + /// * `only_in_b_fields` + /// * `in_both_fields` + /// + /// The result is a set of ranges that can be used to slice those buffers. + fn partitionStaticDispatchConstraints( + self: *const Self, + a_constraints_range: StaticDispatchConstraint.SafeList.Range, + b_constraints_range: StaticDispatchConstraint.SafeList.Range, + ) std.mem.Allocator.Error!PartitionedStaticDispatchConstraints { + const ident_store = self.module_env.getIdentStore(); + const scratch = self.scratch; + + // First sort the fields + const a_constraints = self.types_store.static_dispatch_constraints.sliceRange(a_constraints_range); + std.mem.sort(StaticDispatchConstraint, a_constraints, ident_store, comptime StaticDispatchConstraint.sortByFnNameAsc); + const b_constraints = self.types_store.static_dispatch_constraints.sliceRange(b_constraints_range); + std.mem.sort(StaticDispatchConstraint, b_constraints, ident_store, comptime StaticDispatchConstraint.sortByFnNameAsc); + + // Get the start of index of the new range + const a_constraints_start: u32 = @intCast(scratch.only_in_a_static_dispatch_constraints.len()); + const b_constraints_start: u32 = @intCast(scratch.only_in_b_static_dispatch_constraints.len()); + const both_constraints_start: u32 = @intCast(scratch.in_both_static_dispatch_constraints.len()); + + // Iterate over the fields in order, grouping them + var a_i: usize = 0; + var b_i: usize = 0; + while (a_i < a_constraints.len and b_i < b_constraints.len) { + const a_next = a_constraints[a_i]; + const b_next = b_constraints[b_i]; + const ord = StaticDispatchConstraint.orderByFnName(ident_store, a_next, b_next); + switch (ord) { + .eq => { + _ = try scratch.in_both_static_dispatch_constraints.append(scratch.gpa, TwoStaticDispatchConstraints{ + .a = a_next, + .b = b_next, + }); + a_i = a_i + 1; + b_i = b_i + 1; + }, + .lt => { + _ = try scratch.only_in_a_static_dispatch_constraints.append(scratch.gpa, a_next); + a_i = a_i + 1; + }, + .gt => { + _ = try scratch.only_in_b_static_dispatch_constraints.append(scratch.gpa, b_next); + b_i = b_i + 1; + }, + } + } + + // If b was shorter, add the extra a elems + while (a_i < a_constraints.len) { + const a_next = a_constraints[a_i]; + _ = try scratch.only_in_a_static_dispatch_constraints.append(scratch.gpa, a_next); + a_i = a_i + 1; + } + + // If a was shorter, add the extra b elems + while (b_i < b_constraints.len) { + const b_next = b_constraints[b_i]; + _ = try scratch.only_in_b_static_dispatch_constraints.append(scratch.gpa, b_next); + b_i = b_i + 1; + } + + // Return the ranges + return .{ + .only_in_a = scratch.only_in_a_static_dispatch_constraints.rangeToEnd(a_constraints_start), + .only_in_b = scratch.only_in_b_static_dispatch_constraints.rangeToEnd(b_constraints_start), + .in_both = scratch.in_both_static_dispatch_constraints.rangeToEnd(both_constraints_start), + }; + } + + /// Set error data in scratch & throw + inline fn setUnifyErrAndThrow(self: *Self, err: UnifyErrCtx) Error!void { + self.scratch.setUnifyErr(err); + return error.UnifyErr; + } +}; /// A fatal occurs error pub const UnifyErrCtx = union(enum) { @@ -2672,14 +2331,22 @@ pub const UnifyErrCtx = union(enum) { invalid_tag_union_ext: Var, }; +/// A list of constraint that should apply to concrete type +pub const DeferredConstraintCheck = struct { + var_: Var, + constraints: StaticDispatchConstraint.SafeList.Range, + + pub const SafeList = MkSafeList(@This()); +}; + /// Public helper functions for tests pub fn partitionFields( ident_store: *const Ident.Store, scratch: *Scratch, a_fields_range: RecordFieldSafeList.Range, b_fields_range: RecordFieldSafeList.Range, -) std.mem.Allocator.Error!Unifier(*types_mod.Store).PartitionedRecordFields { - return try Unifier(*types_mod.Store).partitionFields(ident_store, scratch, a_fields_range, b_fields_range); +) std.mem.Allocator.Error!Unifier.PartitionedRecordFields { + return try Unifier.partitionFields(ident_store, scratch, a_fields_range, b_fields_range); } /// Partitions tags from two tag ranges for unification. @@ -2688,8 +2355,8 @@ pub fn partitionTags( scratch: *Scratch, a_tags_range: TagSafeList.Range, b_tags_range: TagSafeList.Range, -) std.mem.Allocator.Error!Unifier(*types_mod.Store).PartitionedTags { - return try Unifier(*types_mod.Store).partitionTags(ident_store, scratch, a_tags_range, b_tags_range); +) std.mem.Allocator.Error!Unifier.PartitionedTags { + return try Unifier.partitionTags(ident_store, scratch, a_tags_range, b_tags_range); } /// A reusable memory arena used across unification calls to avoid per-call allocations. @@ -2722,8 +2389,11 @@ pub fn partitionTags( /// while SafeLists waste some space compared to MultiList, the cost isn't too /// high /// -/// TODO: If canonicalization can ensure that record fields/tags are always sorted -/// then we could switch these to use multi lists. +/// NOTE: Record fields and tags are merged in sorted order during gathering +/// (via mergeSortedExtensionFields/mergeSortedExtensionTags). However, unification +/// itself creates new tag unions that may not be sorted, so partitionFields/partitionTags +/// still perform a final sort. To fully eliminate these sorts, we would need to ensure +/// unifySharedTags also produces sorted output. pub const Scratch = struct { const Self = @This(); @@ -2745,6 +2415,12 @@ pub const Scratch = struct { only_in_b_tags: TagSafeList, in_both_tags: TwoTagsSafeList, + // constraints + deferred_constraints: DeferredConstraintCheck.SafeList, + only_in_a_static_dispatch_constraints: StaticDispatchConstraint.SafeList, + only_in_b_static_dispatch_constraints: StaticDispatchConstraint.SafeList, + in_both_static_dispatch_constraints: TwoStaticDispatchConstraints.SafeList, + // occurs occurs_scratch: occurs.Scratch, @@ -2753,7 +2429,11 @@ pub const Scratch = struct { /// Init scratch pub fn init(gpa: std.mem.Allocator) std.mem.Allocator.Error!Self { - // TODO: Set these based on the heuristics + // Initial capacities are conservative estimates. Lists grow dynamically as needed. + // These values handle common cases without reallocation: + // - fresh_vars: 8 - most unifications create few fresh variables + // - fields/tags: 32 - typical records/unions have fewer than 32 members + // Future optimization: profile real codebases to tune these values. return .{ .gpa = gpa, .fresh_vars = try VarSafeList.initCapacity(gpa, 8), @@ -2765,6 +2445,10 @@ pub const Scratch = struct { .only_in_a_tags = try TagSafeList.initCapacity(gpa, 32), .only_in_b_tags = try TagSafeList.initCapacity(gpa, 32), .in_both_tags = try TwoTagsSafeList.initCapacity(gpa, 32), + .deferred_constraints = try DeferredConstraintCheck.SafeList.initCapacity(gpa, 32), + .only_in_a_static_dispatch_constraints = try StaticDispatchConstraint.SafeList.initCapacity(gpa, 32), + .only_in_b_static_dispatch_constraints = try StaticDispatchConstraint.SafeList.initCapacity(gpa, 32), + .in_both_static_dispatch_constraints = try TwoStaticDispatchConstraints.SafeList.initCapacity(gpa, 32), .occurs_scratch = try occurs.Scratch.init(gpa), .err = null, }; @@ -2781,6 +2465,10 @@ pub const Scratch = struct { self.only_in_a_tags.deinit(self.gpa); self.only_in_b_tags.deinit(self.gpa); self.in_both_tags.deinit(self.gpa); + self.deferred_constraints.deinit(self.gpa); + self.only_in_a_static_dispatch_constraints.deinit(self.gpa); + self.only_in_b_static_dispatch_constraints.deinit(self.gpa); + self.in_both_static_dispatch_constraints.deinit(self.gpa); self.occurs_scratch.deinit(); } @@ -2794,6 +2482,11 @@ pub const Scratch = struct { self.only_in_a_tags.items.clearRetainingCapacity(); self.only_in_b_tags.items.clearRetainingCapacity(); self.in_both_tags.items.clearRetainingCapacity(); + self.deferred_constraints.items.clearRetainingCapacity(); + self.only_in_a_static_dispatch_constraints.items.clearRetainingCapacity(); + self.only_in_b_static_dispatch_constraints.items.clearRetainingCapacity(); + self.in_both_static_dispatch_constraints.items.clearRetainingCapacity(); + self.fresh_vars.items.clearRetainingCapacity(); self.occurs_scratch.reset(); self.err = null; } @@ -2818,6 +2511,118 @@ pub const Scratch = struct { return self.gathered_fields.rangeToEnd(@intCast(start_int)); } + /// Merge sorted extension fields into an already-sorted gathered range. + /// Maintains sorted order and left-bias semantics (base fields take precedence). + /// The range is updated in-place to reflect the new count. + /// Returns whether any new fields were added. + fn mergeSortedExtensionFields( + self: *Self, + range: *RecordFieldSafeList.Range, + ext_names: []const Ident.Idx, + ext_vars: []const Var, + ident_store: *const Ident.Store, + ) std.mem.Allocator.Error!void { + std.debug.assert(ext_names.len == ext_vars.len); + if (ext_names.len == 0) return; + + // Get current gathered fields + const current_fields = self.gathered_fields.sliceRange(range.*); + const current_len = current_fields.len; + + // Count how many extension fields are NOT duplicates + var new_count: usize = 0; + for (ext_names) |ext_name| { + const is_dup = for (current_fields) |existing| { + if (existing.name == ext_name) break true; + } else false; + if (!is_dup) new_count += 1; + } + + if (new_count == 0) return; + + // Allocate space for merged result + try self.gathered_fields.items.ensureUnusedCapacity(self.gpa, new_count); + + // We need to merge in-place. Strategy: + // 1. Append all new (non-duplicate) extension fields to the end + // 2. Then do an in-place merge of the two sorted regions + + // Append non-duplicate extension fields + for (ext_names, ext_vars) |name, var_| { + const is_dup = for (current_fields) |existing| { + if (existing.name == name) break true; + } else false; + if (!is_dup) { + self.gathered_fields.items.appendAssumeCapacity(RecordField{ .name = name, .var_ = var_ }); + } + } + + // Now we have: [sorted_base | sorted_extension] + // Do an in-place merge using the standard merge technique + // Get the slice starting from range.start with the new total length + const start_idx: usize = @intFromEnum(range.start); + const total_len = current_len + new_count; + const items = self.gathered_fields.items.items[start_idx..][0..total_len]; + + // In-place merge: we have [sorted_left | sorted_right] + // Use rotation-based merge which is O(n) for this case + inPlaceMergeFields(items, current_len, ident_store); + + // Update range count + range.count = @intCast(total_len); + } + + /// Merge sorted extension tags into an already-sorted gathered range. + /// Maintains sorted order and left-bias semantics (base tags take precedence). + fn mergeSortedExtensionTags( + self: *Self, + range: *TagSafeList.Range, + ext_names: []const Ident.Idx, + ext_args: []const Var.SafeList.Range, + ident_store: *const Ident.Store, + ) std.mem.Allocator.Error!void { + std.debug.assert(ext_names.len == ext_args.len); + if (ext_names.len == 0) return; + + // Get current gathered tags + const current_tags = self.gathered_tags.sliceRange(range.*); + const current_len = current_tags.len; + + // Count how many extension tags are NOT duplicates + var new_count: usize = 0; + for (ext_names) |ext_name| { + const is_dup = for (current_tags) |existing| { + if (existing.name == ext_name) break true; + } else false; + if (!is_dup) new_count += 1; + } + + if (new_count == 0) return; + + // Allocate space for merged result + try self.gathered_tags.items.ensureUnusedCapacity(self.gpa, new_count); + + // Append non-duplicate extension tags + for (ext_names, ext_args) |name, args| { + const is_dup = for (current_tags) |existing| { + if (existing.name == name) break true; + } else false; + if (!is_dup) { + self.gathered_tags.items.appendAssumeCapacity(Tag{ .name = name, .args = args }); + } + } + + // In-place merge + const start_idx: usize = @intFromEnum(range.start); + const total_len = current_len + new_count; + const items = self.gathered_tags.items.items[start_idx..][0..total_len]; + + inPlaceMergeTags(items, current_len, ident_store); + + // Update range count + range.count = @intCast(total_len); + } + /// Given a multi list of tag and a range, copy from the multi list /// into scratch's gathered fields array fn copyGatherTagsFromMultiList( @@ -2836,11 +2641,13 @@ pub const Scratch = struct { return self.gathered_tags.rangeToEnd(@intCast(start_int)); } - fn appendSliceGatheredFields(self: *Self, fields: []const RecordField) std.mem.Allocator.Error!RecordFieldSafeList.Range { + /// Exposed for tests + pub fn appendSliceGatheredFields(self: *Self, fields: []const RecordField) std.mem.Allocator.Error!RecordFieldSafeList.Range { return try self.gathered_fields.appendSlice(self.gpa, fields); } - fn appendSliceGatheredTags(self: *Self, fields: []const Tag) std.mem.Allocator.Error!TagSafeList.Range { + /// Exposed for tests + pub fn appendSliceGatheredTags(self: *Self, fields: []const Tag) std.mem.Allocator.Error!TagSafeList.Range { return try self.gathered_tags.appendSlice(self.gpa, fields); } @@ -2849,3046 +2656,39 @@ pub const Scratch = struct { } }; -// tests // +/// In-place merge of two sorted regions of record fields. +/// Given an array [left_sorted | right_sorted], produces [merged_sorted]. +/// Uses insertion sort approach which is O(n*m) but efficient for small arrays. +fn inPlaceMergeFields(items: []RecordField, left_len: usize, ident_store: *const Ident.Store) void { + if (left_len == 0 or left_len == items.len) return; -const RootModule = @This(); - -/// A lightweight test harness used in unification and type inference tests. -/// -/// `TestEnv` bundles together the following components: -/// * a module env for holding things like idents -/// * a type store for registering and resolving types -/// * a reusable `Scratch` buffer for managing field partitions and temporary variables -/// -/// This is intended to simplify unit test setup, particularly for unifying records, -/// functions, aliases, and other structured types. -const TestEnv = struct { - const Self = @This(); - - module_env: *ModuleEnv, - snapshots: snapshot_mod.Store, - problems: problem_mod.Store, - scratch: Scratch, - occurs_scratch: occurs.Scratch, - - /// Init everything needed to test unify - /// This includes allocating module_env on the heap - /// - /// TODO: Is heap allocation unideal here? If we want to optimize tests, we - /// could pull module_env's initialization out of here, but this results in - /// slight more verbose setup for each test - fn init(gpa: std.mem.Allocator) std.mem.Allocator.Error!Self { - const module_env = try gpa.create(ModuleEnv); - module_env.* = try ModuleEnv.init(gpa, try gpa.dupe(u8, "")); - try module_env.initCIRFields(gpa, "Test"); - return .{ - .module_env = module_env, - .snapshots = try snapshot_mod.Store.initCapacity(gpa, 16), - .problems = try problem_mod.Store.initCapacity(gpa, 16), - .scratch = try Scratch.init(module_env.gpa), - .occurs_scratch = try occurs.Scratch.init(module_env.gpa), - }; - } - - /// Deinit the test env, including deallocing the module_env from the heap - fn deinit(self: *Self) void { - self.module_env.deinit(); - self.module_env.gpa.destroy(self.module_env); - self.snapshots.deinit(); - self.problems.deinit(self.module_env.gpa); - self.scratch.deinit(); - self.occurs_scratch.deinit(); - } - - /// Helper function to call unify with args from TestEnv - fn unify(self: *Self, a: Var, b: Var) std.mem.Allocator.Error!Result { - return try RootModule.unify( - self.module_env, - &self.module_env.types, - &self.problems, - &self.snapshots, - &self.scratch, - &self.occurs_scratch, - a, - b, - ); - } - - const Error = error{ VarIsNotRoot, IsNotRecord, IsNotTagUnion }; - - /// Get a desc from a root var - fn getDescForRootVar(self: *Self, var_: Var) error{VarIsNotRoot}!Desc { - switch (self.module_env.types.getSlot(var_)) { - .root => |desc_idx| return self.module_env.types.getDesc(desc_idx), - .redirect => return error.VarIsNotRoot, + // Simple approach: insertion sort the right portion into the left + // This is O(n*m) but records typically have few fields + var i = left_len; + while (i < items.len) : (i += 1) { + const key = items[i]; + var j = i; + // Shift elements right until we find the correct position + while (j > 0 and RecordField.orderByName(ident_store, items[j - 1], key) == .gt) { + items[j] = items[j - 1]; + j -= 1; } + items[j] = key; } +} - /// Unwrap a record or throw - fn getRecordOrErr(desc: Desc) error{IsNotRecord}!Record { - return desc.content.unwrapRecord() orelse error.IsNotRecord; +/// In-place merge of two sorted regions of tags. +fn inPlaceMergeTags(items: []Tag, left_len: usize, ident_store: *const Ident.Store) void { + if (left_len == 0 or left_len == items.len) return; + + var i = left_len; + while (i < items.len) : (i += 1) { + const key = items[i]; + var j = i; + while (j > 0 and Tag.orderByName(ident_store, items[j - 1], key) == .gt) { + items[j] = items[j - 1]; + j -= 1; + } + items[j] = key; } - - /// Unwrap a record or throw - fn getTagUnionOrErr(desc: Desc) error{IsNotTagUnion}!TagUnion { - return desc.content.unwrapTagUnion() orelse error.IsNotTagUnion; - } - - fn mkTypeIdent(self: *Self, name: []const u8) std.mem.Allocator.Error!TypeIdent { - const ident_idx = try self.module_env.getIdentStore().insert(self.module_env.gpa, Ident.for_text(name)); - return TypeIdent{ .ident_idx = ident_idx }; - } - - // helpers - rigid var - - fn mkRigidVar(self: *Self, name: []const u8) std.mem.Allocator.Error!Content { - const ident_idx = try self.module_env.getIdentStore().insert(self.module_env.gpa, Ident.for_text(name)); - return Self.mkRigidVarFromIdent(ident_idx); - } - - fn mkRigidVarFromIdent(ident_idx: Ident.Idx) Content { - return .{ .rigid_var = ident_idx }; - } - - // helpers - alias - - fn mkAlias(self: *Self, name: []const u8, backing_var: Var, args: []const Var) std.mem.Allocator.Error!Content { - return try self.module_env.types.mkAlias(try self.mkTypeIdent(name), backing_var, args); - } - - // helpers - nums - - fn mkNum(self: *Self, var_: Var) std.mem.Allocator.Error!Var { - const requirements = Num.IntRequirements{ - .sign_needed = false, - .bits_needed = 0, - }; - return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = .{ .var_ = var_, .requirements = requirements } } } }); - } - - fn mkNumFlex(self: *Self) std.mem.Allocator.Error!Var { - // Create a true flex var that can unify with any numeric type - return try self.module_env.types.fresh(); - } - - fn mkFrac(self: *Self, var_: Var) std.mem.Allocator.Error!Var { - const frac_requirements = Num.FracRequirements{ - .var_ = var_, - .fits_in_f32 = true, - .fits_in_dec = true, - }; - return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_poly = frac_requirements } } }); - } - - fn mkFracFlex(self: *Self) std.mem.Allocator.Error!Var { - const prec_var = try self.module_env.types.fresh(); - const frac_requirements = Num.FracRequirements{ - .fits_in_f32 = true, - .fits_in_dec = true, - }; - return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_poly = .{ .var_ = prec_var, .requirements = frac_requirements } } } }); - } - - fn mkFracRigid(self: *Self, name: []const u8) std.mem.Allocator.Error!Var { - const rigid = try self.module_env.types.freshFromContent(try self.mkRigidVar(name)); - const frac_requirements = Num.FracRequirements{ - .fits_in_f32 = true, - .fits_in_dec = true, - }; - return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_poly = .{ .var_ = rigid, .requirements = frac_requirements } } } }); - } - - fn mkFracPoly(self: *Self, prec: Num.Frac.Precision) std.mem.Allocator.Error!Var { - const prec_var = try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_precision = prec } } }); - const frac_requirements = Num.FracRequirements{ - .fits_in_f32 = true, - .fits_in_dec = true, - }; - return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_poly = .{ .var_ = prec_var, .requirements = frac_requirements } } } }); - } - - fn mkFracExact(self: *Self, prec: Num.Frac.Precision) std.mem.Allocator.Error!Var { - // Create an exact fraction type that only unifies with the same precision - return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_precision = prec } } }); - } - - fn mkInt(self: *Self, var_: Var) std.mem.Allocator.Error!Var { - const int_requirements = Num.IntRequirements{ - .sign_needed = false, - .bits_needed = 0, // 7 bits, the minimum - }; - return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = .{ .var_ = var_, .requirements = int_requirements } } } }); - } - - fn mkIntFlex(self: *Self) std.mem.Allocator.Error!Var { - const prec_var = try self.module_env.types.fresh(); - const int_requirements = Num.IntRequirements{ - .sign_needed = false, - .bits_needed = 0, // 7 bits, the minimum - }; - return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = .{ .var_ = prec_var, .requirements = int_requirements } } } }); - } - - fn mkIntRigid(self: *Self, name: []const u8) std.mem.Allocator.Error!Var { - const rigid = try self.module_env.types.freshFromContent(try self.mkRigidVar(name)); - const int_requirements = Num.IntRequirements{ - .sign_needed = false, - .bits_needed = 0, // 7 bits, the minimum - }; - return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = .{ .var_ = rigid, .requirements = int_requirements } } } }); - } - - fn mkIntPoly(self: *Self, prec: Num.Int.Precision) std.mem.Allocator.Error!Var { - const prec_var = try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_precision = prec } } }); - const int_requirements = Num.IntRequirements{ - .sign_needed = false, - .bits_needed = 0, // 7 bits, the minimum - }; - return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_poly = .{ .var_ = prec_var, .requirements = int_requirements } } } }); - } - - fn mkIntExact(self: *Self, prec: Num.Int.Precision) std.mem.Allocator.Error!Var { - // Create an exact integer type that only unifies with the same precision - return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_precision = prec } } }); - } - - // helpers - structure - tuple - - fn mkTuple(self: *Self, slice: []const Var) std.mem.Allocator.Error!Content { - const elems_range = try self.module_env.types.appendVars(slice); - return Content{ .structure = .{ .tuple = .{ .elems = elems_range } } }; - } - - // helpers - nominal type - - fn mkNominalType(self: *Self, name: []const u8, backing_var: Var, args: []const Var) std.mem.Allocator.Error!Content { - return try self.module_env.types.mkNominal( - try self.mkTypeIdent(name), - backing_var, - args, - Ident.Idx{ .attributes = .{ .effectful = false, .ignored = false, .reassignable = false }, .idx = 0 }, - ); - } - - // helpers - structure - func - - fn mkFuncPure(self: *Self, args: []const Var, ret: Var) std.mem.Allocator.Error!Content { - return try self.module_env.types.mkFuncPure(args, ret); - } - - fn mkFuncEffectful(self: *Self, args: []const Var, ret: Var) std.mem.Allocator.Error!Content { - return try self.module_env.types.mkFuncEffectful(args, ret); - } - - fn mkFuncUnbound(self: *Self, args: []const Var, ret: Var) std.mem.Allocator.Error!Content { - return try self.module_env.types.mkFuncUnbound(args, ret); - } - - fn mkFuncFlex(self: *Self, args: []const Var, ret: Var) std.mem.Allocator.Error!Content { - // For flex functions, we use unbound since we don't know the effectfulness yet - return try self.module_env.types.mkFuncUnbound(args, ret); - } - - // helpers - structure - records - - fn mkRecordField(self: *Self, name: []const u8, var_: Var) std.mem.Allocator.Error!RecordField { - const ident_idx = try self.module_env.getIdentStore().insert(self.module_env.gpa, Ident.for_text(name)); - return Self.mkRecordFieldFromIdent(ident_idx, var_); - } - - fn mkRecordFieldFromIdent(ident_idx: Ident.Idx, var_: Var) RecordField { - return RecordField{ .name = ident_idx, .var_ = var_ }; - } - - const RecordInfo = struct { record: Record, content: Content }; - - fn mkRecord(self: *Self, fields: []const RecordField, ext_var: Var) std.mem.Allocator.Error!RecordInfo { - const fields_range = try self.module_env.types.appendRecordFields(fields); - const record = Record{ .fields = fields_range, .ext = ext_var }; - return .{ .content = Content{ .structure = .{ .record = record } }, .record = record }; - } - - fn mkRecordOpen(self: *Self, fields: []const RecordField) std.mem.Allocator.Error!RecordInfo { - const ext_var = try self.module_env.types.freshFromContent(.{ .flex_var = null }); - return self.mkRecord(fields, ext_var); - } - - fn mkRecordClosed(self: *Self, fields: []const RecordField) std.mem.Allocator.Error!RecordInfo { - const ext_var = try self.module_env.types.freshFromContent(.{ .structure = .empty_record }); - return self.mkRecord(fields, ext_var); - } - - // helpers - structure - tag union - - const TagUnionInfo = struct { tag_union: TagUnion, content: Content }; - - fn mkTagArgs(self: *Self, args: []const Var) std.mem.Allocator.Error!VarSafeList.Range { - return try self.module_env.types.appendVars(args); - } - - fn mkTag(self: *Self, name: []const u8, args: []const Var) std.mem.Allocator.Error!Tag { - const ident_idx = try self.module_env.getIdentStore().insert(self.module_env.gpa, Ident.for_text(name)); - return Tag{ .name = ident_idx, .args = try self.module_env.types.appendVars(args) }; - } - - fn mkTagUnion(self: *Self, tags: []const Tag, ext_var: Var) std.mem.Allocator.Error!TagUnionInfo { - const tags_range = try self.module_env.types.appendTags(tags); - const tag_union = TagUnion{ .tags = tags_range, .ext = ext_var }; - return .{ .content = Content{ .structure = .{ .tag_union = tag_union } }, .tag_union = tag_union }; - } - - fn mkTagUnionOpen(self: *Self, tags: []const Tag) std.mem.Allocator.Error!TagUnionInfo { - const ext_var = try self.module_env.types.freshFromContent(.{ .flex_var = null }); - return self.mkTagUnion(tags, ext_var); - } - - fn mkTagUnionClosed(self: *Self, tags: []const Tag) std.mem.Allocator.Error!TagUnionInfo { - const ext_var = try self.module_env.types.freshFromContent(.{ .structure = .empty_tag_union }); - return self.mkTagUnion(tags, ext_var); - } -}; - -// unification - flex_vars - -// test "unify - identical" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a = try env.module_env.types.fresh(); -// const desc = try env.getDescForRootVar(a); - -// const result = try env.unify(a, a); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(desc, try env.getDescForRootVar(a)); -// } - -// test "unify - both flex vars" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a = try env.module_env.types.fresh(); -// const b = try env.module_env.types.fresh(); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// } - -// test "unify - a is flex_var and b is not" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a = try env.module_env.types.fresh(); -// const b = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i8 } }); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// } - -// // unification - rigid - -// test "rigid_var - unifies with flex_var" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const rigid = try env.mkRigidVar("a"); -// const a = try env.module_env.types.freshFromContent(.{ .flex_var = null }); -// const b = try env.module_env.types.freshFromContent(rigid); - -// const result = try env.unify(a, b); -// try std.testing.expectEqual(true, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(rigid, (try env.getDescForRootVar(b)).content); -// } - -// test "rigid_var - unifies with flex_var (other way)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const rigid = try env.mkRigidVar("a"); -// const a = try env.module_env.types.freshFromContent(rigid); -// const b = try env.module_env.types.freshFromContent(.{ .flex_var = null }); - -// const result = try env.unify(a, b); -// try std.testing.expectEqual(true, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(rigid, (try env.getDescForRootVar(b)).content); -// } - -// test "rigid_var - cannot unify with alias (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const alias = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const rigid = try env.module_env.types.freshFromContent(try env.mkRigidVar("a")); - -// const result = try env.unify(alias, rigid); -// try std.testing.expectEqual(false, result.isOk()); -// } - -// test "rigid_var - cannot unify with identical ident str (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const rigid1 = try env.module_env.types.freshFromContent(try env.mkRigidVar("a")); -// const rigid2 = try env.module_env.types.freshFromContent(try env.mkRigidVar("a")); - -// const result = try env.unify(rigid1, rigid2); -// try std.testing.expectEqual(false, result.isOk()); -// } - -// // unification - aliases - -// test "unify - alias with same args" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const bool_ = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i8 } }); - -// // Create alias `a` with its backing var and args in sequence -// const a_backing_var = try env.module_env.types.freshFromContent(try env.mkTuple(&[_]Var{ str, bool_ })); -// const a_alias = try env.mkAlias("AliasName", a_backing_var, &[_]Var{ str, bool_ }); -// const a = try env.module_env.types.freshFromContent(a_alias); - -// // Create alias `b` with its backing var and args in sequence -// const b_backing_var = try env.module_env.types.freshFromContent(try env.mkTuple(&[_]Var{ str, bool_ })); -// const b_alias = try env.mkAlias("AliasName", b_backing_var, &[_]Var{ str, bool_ }); -// const b = try env.module_env.types.freshFromContent(b_alias); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(b_alias, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - aliases with different names but same backing" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// // Create alias `a` with its backing var and arg -// const a_backing_var = try env.module_env.types.freshFromContent(try env.mkTuple(&[_]Var{str})); -// const a_alias = try env.mkAlias("AliasA", a_backing_var, &[_]Var{str}); -// const a = try env.module_env.types.freshFromContent(a_alias); - -// // Create alias `b` with its backing var and arg -// const b_backing_var = try env.module_env.types.freshFromContent(try env.mkTuple(&[_]Var{str})); -// const b_alias = try env.mkAlias("AliasB", b_backing_var, &[_]Var{str}); -// const b = try env.module_env.types.freshFromContent(b_alias); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(a_alias, (try env.getDescForRootVar(a)).content); -// try std.testing.expectEqual(b_alias, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - alias with different args (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const bool_ = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i8 } }); - -// // Create alias `a` with its backing var and arg -// const a_backing_var = try env.module_env.types.freshFromContent(try env.mkTuple(&[_]Var{ str, bool_ })); -// const a_alias = try env.mkAlias("Alias", a_backing_var, &[_]Var{str}); -// const a = try env.module_env.types.freshFromContent(a_alias); - -// // Create alias `b` with its backing var and arg -// const b_backing_var = try env.module_env.types.freshFromContent(try env.mkTuple(&[_]Var{ str, bool_ })); -// const b_alias = try env.mkAlias("Alias", b_backing_var, &[_]Var{bool_}); -// const b = try env.module_env.types.freshFromContent(b_alias); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - alias with flex" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const bool_ = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i8 } }); - -// const a_backing_var = try env.module_env.types.freshFromContent(try env.mkTuple(&[_]Var{ str, bool_ })); // backing var -// const a_alias = try env.mkAlias("Alias", a_backing_var, &[_]Var{bool_}); - -// const a = try env.module_env.types.freshFromContent(a_alias); -// const b = try env.module_env.types.fresh(); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(a_alias, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/flex_vars - -// test "unify - a is builtin and b is flex_var" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = Content{ .structure = .str }; - -// const a = try env.module_env.types.freshFromContent(str); -// const b = try env.module_env.types.fresh(); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(str, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - a is flex_var and b is builtin" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = Content{ .structure = .str }; - -// const a = try env.module_env.types.fresh(); -// const b = try env.module_env.types.freshFromContent(str); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(str, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/structure - builtin - -// test "unify - a & b are both str" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = Content{ .structure = .str }; - -// const a = try env.module_env.types.freshFromContent(str); -// const b = try env.module_env.types.freshFromContent(str); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(str, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - a & b are diff (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = Content{ .structure = .str }; -// const int = Content{ .structure = .{ .num = Num.int_i8 } }; - -// const a = try env.module_env.types.freshFromContent(int); -// const b = try env.module_env.types.freshFromContent(str); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - a & b box with same arg unify" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = Content{ .structure = .str }; -// const str_var = try env.module_env.types.freshFromContent(str); - -// const box_str = Content{ .structure = .{ .box = str_var } }; - -// const a = try env.module_env.types.freshFromContent(box_str); -// const b = try env.module_env.types.freshFromContent(box_str); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(box_str, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - a & b box with diff args (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = Content{ .structure = .str }; -// const str_var = try env.module_env.types.freshFromContent(str); - -// const i64_ = Content{ .structure = .{ .num = Num.int_i64 } }; -// const i64_var = try env.module_env.types.freshFromContent(i64_); - -// const box_str = Content{ .structure = .{ .box = str_var } }; -// const box_i64 = Content{ .structure = .{ .box = i64_var } }; - -// const a = try env.module_env.types.freshFromContent(box_str); -// const b = try env.module_env.types.freshFromContent(box_i64); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - a & b list with same arg unify" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = Content{ .structure = .str }; -// const str_var = try env.module_env.types.freshFromContent(str); - -// const list_str = Content{ .structure = .{ .list = str_var } }; - -// const a = try env.module_env.types.freshFromContent(list_str); -// const b = try env.module_env.types.freshFromContent(list_str); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(list_str, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - a & b list with diff args (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = Content{ .structure = .str }; -// const str_var = try env.module_env.types.freshFromContent(str); - -// const u8_ = Content{ .structure = .{ .num = Num.int_u8 } }; -// const u8_var = try env.module_env.types.freshFromContent(u8_); - -// const list_str = Content{ .structure = .{ .list = str_var } }; -// const list_u8 = Content{ .structure = .{ .list = u8_var } }; - -// const a = try env.module_env.types.freshFromContent(list_str); -// const b = try env.module_env.types.freshFromContent(list_u8); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/structure - tuple - -// test "unify - a & b are same tuple" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = Content{ .structure = .str }; -// const str_var = try env.module_env.types.freshFromContent(str); - -// const bool_ = Content{ .structure = .{ .num = Num.int_i8 } }; -// const bool_var = try env.module_env.types.freshFromContent(bool_); - -// const tuple_str_bool = try env.mkTuple(&[_]Var{ str_var, bool_var }); - -// const a = try env.module_env.types.freshFromContent(tuple_str_bool); -// const b = try env.module_env.types.freshFromContent(tuple_str_bool); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(tuple_str_bool, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - a & b are tuples with args flipped (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = Content{ .structure = .str }; -// const str_var = try env.module_env.types.freshFromContent(str); - -// const bool_ = Content{ .structure = .{ .num = Num.int_i8 } }; -// const bool_var = try env.module_env.types.freshFromContent(bool_); - -// const tuple_str_bool = try env.mkTuple(&[_]Var{ str_var, bool_var }); -// const tuple_bool_str = try env.mkTuple(&[_]Var{ bool_var, str_var }); - -// const a = try env.module_env.types.freshFromContent(tuple_str_bool); -// const b = try env.module_env.types.freshFromContent(tuple_bool_str); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/structure - compact/compact - -// test "unify - two compact ints" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = Content{ .structure = .{ .num = Num.int_i32 } }; -// const a = try env.module_env.types.freshFromContent(int_i32); -// const b = try env.module_env.types.freshFromContent(int_i32); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(int_i32, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - two compact ints (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i32 } }); -// const b = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - two compact fracs" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const frac_f32 = Content{ .structure = .{ .num = Num.frac_f32 } }; -// const a = try env.module_env.types.freshFromContent(frac_f32); -// const b = try env.module_env.types.freshFromContent(frac_f32); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(frac_f32, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - two compact fracs (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.frac_f32 } }); -// const b = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.frac_dec } }); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/structure - poly/poly - -// test "unify - two poly ints" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a = try env.mkIntPoly(Num.Int.Precision.u8); -// const b = try env.mkIntPoly(Num.Int.Precision.u8); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// } - -// test "unify - two poly ints (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a = try env.mkIntPoly(Num.Int.Precision.u8); -// const b = try env.mkIntPoly(Num.Int.Precision.i128); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - two poly fracs" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a = try env.mkFracPoly(Num.Frac.Precision.f64); -// const b = try env.mkFracPoly(Num.Frac.Precision.f64); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// } - -// test "unify - two poly fracs (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a = try env.mkFracPoly(Num.Frac.Precision.f32); -// const b = try env.mkFracPoly(Num.Frac.Precision.f64); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/structure - poly/compact_int - -// test "unify - Num(flex) and compact int" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = Content{ .structure = .{ .num = Num.int_i32 } }; -// const a = try env.mkNumFlex(); -// const b = try env.module_env.types.freshFromContent(int_i32); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(int_i32, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - Num(Int(flex)) and compact int" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = Content{ .structure = .{ .num = Num.int_i32 } }; -// const a = try env.mkIntFlex(); -// const b = try env.module_env.types.freshFromContent(int_i32); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(int_i32, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - Num(Int(U8)) and compact int U8" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_u8 = Content{ .structure = .{ .num = Num.int_u8 } }; -// const a = try env.mkIntExact(Num.Int.Precision.u8); -// const b = try env.module_env.types.freshFromContent(int_u8); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(int_u8, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - Num(Int(U8)) and compact int I32 (fails)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = Content{ .structure = .{ .num = Num.int_i32 } }; -// const a = try env.mkIntExact(Num.Int.Precision.u8); -// const b = try env.module_env.types.freshFromContent(int_i32); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/structure - poly/compact_frac - -// test "unify - Num(flex) and compact frac" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const frac_f32 = Content{ .structure = .{ .num = Num.frac_f32 } }; -// const a = try env.mkNumFlex(); -// const b = try env.module_env.types.freshFromContent(frac_f32); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(frac_f32, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - Num(Frac(flex)) and compact frac" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const frac_f32 = Content{ .structure = .{ .num = Num.frac_f32 } }; -// const a = try env.mkFracFlex(); -// const b = try env.module_env.types.freshFromContent(frac_f32); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(frac_f32, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - Num(Frac(Dec)) and compact frac Dec" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const frac_dec = Content{ .structure = .{ .num = Num.frac_dec } }; -// const a = try env.mkFracExact(Num.Frac.Precision.dec); -// const b = try env.module_env.types.freshFromContent(frac_dec); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(frac_dec, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - Num(Frac(F32)) and compact frac Dec (fails)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const frac_f32 = Content{ .structure = .{ .num = Num.frac_f32 } }; -// const a = try env.mkFracExact(Num.Frac.Precision.dec); -// const b = try env.module_env.types.freshFromContent(frac_f32); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/structure - compact_int/poly - -// test "unify - compact int and Num(flex)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = Content{ .structure = .{ .num = Num.int_i32 } }; -// const a = try env.module_env.types.freshFromContent(int_i32); -// const b = try env.mkNumFlex(); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(int_i32, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - compact int and Num(Int(flex))" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = Content{ .structure = .{ .num = Num.int_i32 } }; -// const a = try env.module_env.types.freshFromContent(int_i32); -// const b = try env.mkIntFlex(); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(int_i32, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - compact int and U8 Num(Int(U8))" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_u8 = Content{ .structure = .{ .num = Num.int_u8 } }; -// const a = try env.module_env.types.freshFromContent(int_u8); -// const b = try env.mkIntExact(Num.Int.Precision.u8); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(int_u8, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - compact int U8 and Num(Int(I32)) (fails)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = Content{ .structure = .{ .num = Num.int_i32 } }; -// const a = try env.module_env.types.freshFromContent(int_i32); -// const b = try env.mkIntExact(Num.Int.Precision.u8); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/structure - compact_frac/poly - -// test "unify - compact frac and Num(flex)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const frac_f32 = Content{ .structure = .{ .num = Num.frac_f32 } }; -// const a = try env.module_env.types.freshFromContent(frac_f32); -// const b = try env.mkNumFlex(); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(frac_f32, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - compact frac and Num(Frac(flex))" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const frac_f32 = Content{ .structure = .{ .num = Num.frac_f32 } }; -// const a = try env.module_env.types.freshFromContent(frac_f32); -// const b = try env.mkFracFlex(); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(frac_f32, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - compact frac and Dec Num(Frac(Dec))" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const frac_dec = Content{ .structure = .{ .num = Num.frac_dec } }; -// const a = try env.module_env.types.freshFromContent(frac_dec); -// const b = try env.mkFracExact(Num.Frac.Precision.dec); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(frac_dec, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - compact frac Dec and Num(Frac(F32)) (fails)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const frac_f32 = Content{ .structure = .{ .num = Num.frac_f32 } }; -// const a = try env.module_env.types.freshFromContent(frac_f32); -// const b = try env.mkFracExact(Num.Frac.Precision.dec); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/structure - poly/poly rigid - -// test "unify - Num(rigid) and Num(rigid)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const rigid = try env.module_env.types.freshFromContent(try env.mkRigidVar("b")); -// const requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = 0, -// }; -// const num = Content{ .structure = .{ .num = .{ .num_poly = .{ .var_ = rigid, .requirements = requirements } } } }; -// const a = try env.module_env.types.freshFromContent(num); -// const b = try env.module_env.types.freshFromContent(num); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(true, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(num, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - Num(rigid_a) and Num(rigid_b)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const rigid_a = try env.module_env.types.freshFromContent(try env.mkRigidVar("a")); -// const rigid_b = try env.module_env.types.freshFromContent(try env.mkRigidVar("b")); - -// const int_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = 0, -// }; -// const a = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = .{ .var_ = rigid_a, .requirements = int_requirements } } } }); -// const b = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = .{ .var_ = rigid_b, .requirements = int_requirements } } } }); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - Num(Int(rigid)) and Num(Int(rigid))" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const rigid = try env.module_env.types.freshFromContent(try env.mkRigidVar("b")); -// const int_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = 0, -// }; -// _ = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_poly = .{ .var_ = rigid, .requirements = int_requirements } } } }); -// const num = Content{ .structure = .{ .num = .{ .num_poly = .{ .var_ = rigid, .requirements = int_requirements } } } }; -// const a = try env.module_env.types.freshFromContent(num); -// const b = try env.module_env.types.freshFromContent(num); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(true, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(num, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - Num(Frac(rigid)) and Num(Frac(rigid))" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const rigid = try env.module_env.types.freshFromContent(try env.mkRigidVar("b")); -// const frac_requirements = Num.FracRequirements{ -// .fits_in_f32 = true, -// .fits_in_dec = true, -// }; -// const frac_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_poly = .{ .var_ = rigid, .requirements = frac_requirements } } } }); -// const int_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = 0, -// }; -// const num = Content{ .structure = .{ .num = .{ .num_poly = .{ .var_ = frac_var, .requirements = int_requirements } } } }; -// const a = try env.module_env.types.freshFromContent(num); -// const b = try env.module_env.types.freshFromContent(num); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(true, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(num, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/structure - compact/poly rigid - -// test "unify - compact int U8 and Num(Int(rigid)) (fails)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_u8 = Content{ .structure = .{ .num = Num.int_u8 } }; -// const a = try env.module_env.types.freshFromContent(int_u8); -// const b = try env.mkFracRigid("a"); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - compact frac Dec and Num(Frac(rigid)) (fails)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const frac_f32 = Content{ .structure = .{ .num = Num.frac_f32 } }; -// const a = try env.module_env.types.freshFromContent(frac_f32); -// const b = try env.mkFracRigid("a"); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/structure - poly/compact rigid - -// test "unify - Num(Int(rigid)) and compact int U8 (fails)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_u8 = Content{ .structure = .{ .num = Num.int_u8 } }; -// const a = try env.mkFracRigid("a"); -// const b = try env.module_env.types.freshFromContent(int_u8); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - Num(Frac(rigid)) and compact frac Dec (fails)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const frac_f32 = Content{ .structure = .{ .num = Num.frac_f32 } }; -// const a = try env.mkFracRigid("a"); -// const b = try env.module_env.types.freshFromContent(frac_f32); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// // unification - structure/structure - func - -// test "unify - func are same" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i32 } }); -// const num_flex = try env.module_env.types.fresh(); -// const requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = 0, -// }; -// const num = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = .{ .var_ = num_flex, .requirements = requirements } } } }); -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const func = try env.mkFuncFlex(&[_]Var{ str, num }, int_i32); - -// const a = try env.module_env.types.freshFromContent(func); -// const b = try env.module_env.types.freshFromContent(func); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(func, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - funcs have diff return args (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i32 } }); -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const a = try env.module_env.types.freshFromContent(try env.mkFuncFlex(&[_]Var{int_i32}, str)); -// const b = try env.module_env.types.freshFromContent(try env.mkFuncFlex(&[_]Var{str}, str)); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - funcs have diff return types (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i32 } }); -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const a = try env.module_env.types.freshFromContent(try env.mkFuncFlex(&[_]Var{str}, int_i32)); -// const b = try env.module_env.types.freshFromContent(try env.mkFuncFlex(&[_]Var{str}, str)); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - same funcs pure" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i32 } }); -// const int_poly_var = try env.module_env.types.fresh(); -// const int_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = 0, -// }; -// const int_poly = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_poly = .{ .var_ = int_poly_var, .requirements = int_requirements } } } }); -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const func = try env.mkFuncPure(&[_]Var{ str, int_poly }, int_i32); - -// const a = try env.module_env.types.freshFromContent(func); -// const b = try env.module_env.types.freshFromContent(func); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(func, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - same funcs effectful" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i32 } }); -// const int_poly_var = try env.module_env.types.fresh(); -// const int_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = 0, -// }; -// const int_poly = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_poly = .{ .var_ = int_poly_var, .requirements = int_requirements } } } }); -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const func = try env.mkFuncEffectful(&[_]Var{ str, int_poly }, int_i32); - -// const a = try env.module_env.types.freshFromContent(func); -// const b = try env.module_env.types.freshFromContent(func); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(func, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - same funcs first eff, second pure (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i32 } }); -// const int_poly_var = try env.module_env.types.fresh(); -// const int_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = 0, -// }; -// const int_poly = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_poly = .{ .var_ = int_poly_var, .requirements = int_requirements } } } }); -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const pure_func = try env.mkFuncPure(&[_]Var{ str, int_poly }, int_i32); -// const eff_func = try env.mkFuncEffectful(&[_]Var{ str, int_poly }, int_i32); - -// const a = try env.module_env.types.freshFromContent(eff_func); -// const b = try env.module_env.types.freshFromContent(pure_func); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - same funcs first pure, second eff" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const int_i32 = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i32 } }); -// const int_poly_var = try env.module_env.types.fresh(); -// const int_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = 0, -// }; -// const int_poly = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_poly = .{ .var_ = int_poly_var, .requirements = int_requirements } } } }); -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const pure_func = try env.mkFuncPure(&[_]Var{ str, int_poly }, int_i32); -// const eff_func = try env.mkFuncEffectful(&[_]Var{ str, int_poly }, int_i32); - -// const a = try env.module_env.types.freshFromContent(pure_func); -// const b = try env.module_env.types.freshFromContent(eff_func); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// } - -// test "unify - first is flex, second is func" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const tag_payload = try env.module_env.types.fresh(); -// const tag = try env.mkTag("Some", &[_]Var{tag_payload}); -// const backing_var = try env.module_env.types.freshFromContent((try env.mkTagUnionOpen(&[_]Tag{tag})).content); -// const nominal_type = try env.module_env.types.freshFromContent(try env.mkNominalType("List", backing_var, &[_]Var{})); -// const arg = try env.module_env.types.fresh(); -// const func = try env.mkFuncUnbound(&[_]Var{arg}, nominal_type); - -// const a = try env.module_env.types.fresh(); -// const b = try env.module_env.types.freshFromContent(func); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(true, result.isOk()); -// } - -// // unification - structure/structure - nominal type - -// test "unify - a & b are both the same nominal type" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const arg = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); - -// const a_backing_var = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const a = try env.module_env.types.freshFromContent(try env.mkNominalType("MyType", a_backing_var, &[_]Var{arg})); - -// const b_backing_var = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const b_nominal = try env.mkNominalType("MyType", b_backing_var, &[_]Var{arg}); -// const b = try env.module_env.types.freshFromContent(b_nominal); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(b_nominal, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - a & b are diff nominal types (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const arg = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); - -// const a_backing_var = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const a = try env.module_env.types.freshFromContent(try env.mkNominalType("MyType", a_backing_var, &[_]Var{arg})); - -// const b_backing_var = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const b = try env.module_env.types.freshFromContent(try env.mkNominalType("AnotherType", b_backing_var, &[_]Var{arg})); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - a & b are both the same nominal type with diff args (fail)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const arg_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); -// const str_var = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const a_backing = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const a = try env.module_env.types.freshFromContent(try env.mkNominalType("MyType", a_backing, &[_]Var{arg_var})); - -// const b_backing = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const b = try env.module_env.types.freshFromContent(try env.mkNominalType("MyType", b_backing, &[_]Var{str_var})); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(.err, (try env.getDescForRootVar(b)).content); -// } - -// // unification - records - partition fields - -// test "partitionFields - same record" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const field_x = try env.mkRecordField("field_x", @enumFromInt(0)); -// const field_y = try env.mkRecordField("field_y", @enumFromInt(1)); - -// const range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{ field_x, field_y }); - -// const result = try partitionFields(&env.module_env.getIdentStore(), &env.scratch, range, range); - -// try std.testing.expectEqual(0, result.only_in_a.len()); -// try std.testing.expectEqual(0, result.only_in_b.len()); -// try std.testing.expectEqual(2, result.in_both.len()); - -// const both_slice = env.scratch.in_both_fields.sliceRange(result.in_both); -// try std.testing.expectEqual(field_x, both_slice[0].a); -// try std.testing.expectEqual(field_x, both_slice[0].b); -// try std.testing.expectEqual(field_y, both_slice[1].a); -// try std.testing.expectEqual(field_y, both_slice[1].b); -// } - -// test "partitionFields - disjoint fields" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a1 = try env.mkRecordField("a1", @enumFromInt(0)); -// const a2 = try env.mkRecordField("a2", @enumFromInt(1)); -// const b1 = try env.mkRecordField("b1", @enumFromInt(2)); - -// const a_range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{ a1, a2 }); -// const b_range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{b1}); - -// const result = try partitionFields(&env.module_env.getIdentStore(), &env.scratch, a_range, b_range); - -// try std.testing.expectEqual(2, result.only_in_a.len()); -// try std.testing.expectEqual(1, result.only_in_b.len()); -// try std.testing.expectEqual(0, result.in_both.len()); - -// const only_in_a_slice = env.scratch.only_in_a_fields.sliceRange(result.only_in_a); -// try std.testing.expectEqual(a1, only_in_a_slice[0]); -// try std.testing.expectEqual(a2, only_in_a_slice[1]); - -// const only_in_b_slice = env.scratch.only_in_b_fields.sliceRange(result.only_in_b); -// try std.testing.expectEqual(b1, only_in_b_slice[0]); -// } - -// test "partitionFields - overlapping fields" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a1 = try env.mkRecordField("a1", @enumFromInt(0)); -// const both = try env.mkRecordField("both", @enumFromInt(1)); -// const b1 = try env.mkRecordField("b1", @enumFromInt(2)); - -// const a_range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{ a1, both }); -// const b_range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{ b1, both }); - -// const result = try partitionFields(&env.module_env.getIdentStore(), &env.scratch, a_range, b_range); - -// try std.testing.expectEqual(1, result.only_in_a.len()); -// try std.testing.expectEqual(1, result.only_in_b.len()); -// try std.testing.expectEqual(1, result.in_both.len()); - -// const both_slice = env.scratch.in_both_fields.sliceRange(result.in_both); -// try std.testing.expectEqual(both, both_slice[0].a); -// try std.testing.expectEqual(both, both_slice[0].b); - -// const only_in_a_slice = env.scratch.only_in_a_fields.sliceRange(result.only_in_a); -// try std.testing.expectEqual(a1, only_in_a_slice[0]); - -// const only_in_b_slice = env.scratch.only_in_b_fields.sliceRange(result.only_in_b); -// try std.testing.expectEqual(b1, only_in_b_slice[0]); -// } - -// test "partitionFields - reordering is normalized" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const f1 = try env.mkRecordField("f1", @enumFromInt(0)); -// const f2 = try env.mkRecordField("f2", @enumFromInt(1)); -// const f3 = try env.mkRecordField("f3", @enumFromInt(2)); - -// const a_range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{ f3, f1, f2 }); -// const b_range = try env.scratch.appendSliceGatheredFields(&[_]RecordField{ f1, f2, f3 }); - -// const result = try partitionFields(&env.module_env.getIdentStore(), &env.scratch, a_range, b_range); - -// try std.testing.expectEqual(0, result.only_in_a.len()); -// try std.testing.expectEqual(0, result.only_in_b.len()); -// try std.testing.expectEqual(3, result.in_both.len()); - -// const both = env.scratch.in_both_fields.sliceRange(result.in_both); -// try std.testing.expectEqual(f1, both[0].a); -// try std.testing.expectEqual(f1, both[0].b); -// try std.testing.expectEqual(f2, both[1].a); -// try std.testing.expectEqual(f2, both[1].b); -// try std.testing.expectEqual(f3, both[2].a); -// try std.testing.expectEqual(f3, both[2].b); -// } - -// // unification - structure/structure - records closed - -// test "unify - identical closed records" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const fields = [_]RecordField{try env.mkRecordField("a", str)}; -// const record_data = try env.mkRecordClosed(&fields); -// const record_data_fields = env.module_env.types.record_fields.sliceRange(record_data.record.fields); - -// const a = try env.module_env.types.freshFromContent(record_data.content); -// const b = try env.module_env.types.freshFromContent(record_data.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// const b_record = try TestEnv.getRecordOrErr(try env.getDescForRootVar(b)); -// const b_record_fields = env.module_env.types.record_fields.sliceRange(b_record.fields); -// try std.testing.expectEqualSlices(Ident.Idx, record_data_fields.items(.name), b_record_fields.items(.name)); -// try std.testing.expectEqualSlices(Var, record_data_fields.items(.var_), b_record_fields.items(.var_)); -// } - -// test "unify - closed record mismatch on diff fields (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const field1 = try env.mkRecordField("field1", str); -// const field2 = try env.mkRecordField("field2", str); - -// const a_record_data = try env.mkRecordClosed(&[_]RecordField{ field1, field2 }); -// const a = try env.module_env.types.freshFromContent(a_record_data.content); - -// const b_record_data = try env.mkRecordClosed(&[_]RecordField{field1}); -// const b = try env.module_env.types.freshFromContent(b_record_data.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// const desc_b = try env.getDescForRootVar(b); -// try std.testing.expectEqual(Content.err, desc_b.content); -// } - -// // unification - structure/structure - records open - -// test "unify - identical open records" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const field_shared = try env.mkRecordField("x", str); - -// const a_rec_data = try env.mkRecordOpen(&[_]RecordField{field_shared}); -// const a = try env.module_env.types.freshFromContent(a_rec_data.content); -// const b_rec_data = try env.mkRecordOpen(&[_]RecordField{field_shared}); -// const b = try env.module_env.types.freshFromContent(b_rec_data.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// // check that the update var at b is correct - -// const b_record = try TestEnv.getRecordOrErr(try env.getDescForRootVar(b)); -// try std.testing.expectEqual(1, b_record.fields.len()); -// const b_record_fields = env.module_env.types.getRecordFieldsSlice(b_record.fields); -// try std.testing.expectEqual(field_shared.name, b_record_fields.items(.name)[0]); -// try std.testing.expectEqual(field_shared.var_, b_record_fields.items(.var_)[0]); - -// const b_ext = env.module_env.types.resolveVar(b_record.ext).desc.content; -// try std.testing.expectEqual(Content{ .flex_var = null }, b_ext); - -// // check that fresh vars are correct - -// try std.testing.expectEqual(0, env.scratch.fresh_vars.len()); -// } - -// test "unify - open record a extends b" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const int = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); - -// const field_shared = try env.mkRecordField("x", str); -// const field_a_only = try env.mkRecordField("y", int); - -// const a_rec_data = try env.mkRecordOpen(&[_]RecordField{ field_shared, field_a_only }); -// const a = try env.module_env.types.freshFromContent(a_rec_data.content); -// const b_rec_data = try env.mkRecordOpen(&[_]RecordField{field_shared}); -// const b = try env.module_env.types.freshFromContent(b_rec_data.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// // check that the update var at b is correct - -// const b_record = try TestEnv.getRecordOrErr(try env.getDescForRootVar(b)); -// try std.testing.expectEqual(1, b_record.fields.len()); -// const b_record_fields = env.module_env.types.getRecordFieldsSlice(b_record.fields); -// try std.testing.expectEqual(field_shared.name, b_record_fields.items(.name)[0]); -// try std.testing.expectEqual(field_shared.var_, b_record_fields.items(.var_)[0]); - -// try std.testing.expectEqual(1, env.scratch.fresh_vars.len()); -// try std.testing.expectEqual(env.scratch.fresh_vars.get(@enumFromInt(0)).*, b_record.ext); - -// const b_ext_record = try TestEnv.getRecordOrErr(env.module_env.types.resolveVar(b_record.ext).desc); -// try std.testing.expectEqual(1, b_ext_record.fields.len()); -// const b_ext_record_fields = env.module_env.types.getRecordFieldsSlice(b_ext_record.fields); -// try std.testing.expectEqual(field_a_only.name, b_ext_record_fields.items(.name)[0]); -// try std.testing.expectEqual(field_a_only.var_, b_ext_record_fields.items(.var_)[0]); - -// const b_ext_ext = env.module_env.types.resolveVar(b_ext_record.ext).desc.content; -// try std.testing.expectEqual(Content{ .flex_var = null }, b_ext_ext); - -// // check that fresh vars are correct - -// try std.testing.expectEqual(1, env.scratch.fresh_vars.len()); -// try std.testing.expectEqual(b_record.ext, env.scratch.fresh_vars.items.items[0]); -// } - -// test "unify - open record b extends a" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const int = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); - -// const field_shared = try env.mkRecordField("field_shared", str); -// const field_b_only = try env.mkRecordField("field_b_only", int); - -// const a_rec_data = try env.mkRecordOpen(&[_]RecordField{field_shared}); -// const a = try env.module_env.types.freshFromContent(a_rec_data.content); -// const b_rec_data = try env.mkRecordOpen(&[_]RecordField{ field_shared, field_b_only }); -// const b = try env.module_env.types.freshFromContent(b_rec_data.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// // check that the update var at b is correct - -// const b_record = try TestEnv.getRecordOrErr(try env.getDescForRootVar(b)); -// try std.testing.expectEqual(1, b_record.fields.len()); -// const b_record_fields = env.module_env.types.getRecordFieldsSlice(b_record.fields); -// try std.testing.expectEqual(field_shared.name, b_record_fields.items(.name)[0]); -// try std.testing.expectEqual(field_shared.var_, b_record_fields.items(.var_)[0]); - -// const b_ext_record = try TestEnv.getRecordOrErr(env.module_env.types.resolveVar(b_record.ext).desc); -// try std.testing.expectEqual(1, b_ext_record.fields.len()); -// const b_ext_record_fields = env.module_env.types.getRecordFieldsSlice(b_ext_record.fields); -// try std.testing.expectEqual(field_b_only.name, b_ext_record_fields.items(.name)[0]); -// try std.testing.expectEqual(field_b_only.var_, b_ext_record_fields.items(.var_)[0]); - -// const b_ext_ext = env.module_env.types.resolveVar(b_ext_record.ext).desc.content; -// try std.testing.expectEqual(Content{ .flex_var = null }, b_ext_ext); - -// // check that fresh vars are correct - -// try std.testing.expectEqual(1, env.scratch.fresh_vars.len()); -// try std.testing.expectEqual(b_record.ext, env.scratch.fresh_vars.items.items[0]); -// } - -// test "unify - both extend open record" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const int = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); -// const bool_ = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i8 } }); - -// const field_shared = try env.mkRecordField("x", str); -// const field_a_only = try env.mkRecordField("y", int); -// const field_b_only = try env.mkRecordField("z", bool_); - -// const a_rec_data = try env.mkRecordOpen(&[_]RecordField{ field_shared, field_a_only }); -// const a = try env.module_env.types.freshFromContent(a_rec_data.content); -// const b_rec_data = try env.mkRecordOpen(&[_]RecordField{ field_shared, field_b_only }); -// const b = try env.module_env.types.freshFromContent(b_rec_data.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// // check that the update var at b is correct - -// const b_record = try TestEnv.getRecordOrErr(try env.getDescForRootVar(b)); -// try std.testing.expectEqual(3, b_record.fields.len()); -// const b_record_fields = env.module_env.types.getRecordFieldsSlice(b_record.fields); -// try std.testing.expectEqual(field_shared, b_record_fields.get(0)); -// try std.testing.expectEqual(field_a_only, b_record_fields.get(1)); -// try std.testing.expectEqual(field_b_only, b_record_fields.get(2)); - -// const b_ext = env.module_env.types.resolveVar(b_record.ext).desc.content; -// try std.testing.expectEqual(Content{ .flex_var = null }, b_ext); - -// // check that fresh vars are correct - -// try std.testing.expectEqual(3, env.scratch.fresh_vars.len()); - -// const only_a_var = env.scratch.fresh_vars.get(@enumFromInt(0)).*; -// const only_a_record = try TestEnv.getRecordOrErr(env.module_env.types.resolveVar(only_a_var).desc); -// try std.testing.expectEqual(1, only_a_record.fields.len()); -// const only_a_record_fields = env.module_env.types.getRecordFieldsSlice(only_a_record.fields); -// try std.testing.expectEqual(field_a_only, only_a_record_fields.get(0)); - -// const only_b_var = env.scratch.fresh_vars.get(@enumFromInt(1)).*; -// const only_b_record = try TestEnv.getRecordOrErr(env.module_env.types.resolveVar(only_b_var).desc); -// try std.testing.expectEqual(1, only_b_record.fields.len()); -// const only_b_record_fields = env.module_env.types.getRecordFieldsSlice(only_b_record.fields); -// try std.testing.expectEqual(field_b_only, only_b_record_fields.get(0)); - -// const ext_var = env.scratch.fresh_vars.get(@enumFromInt(2)).*; -// const ext_content = env.module_env.types.resolveVar(ext_var).desc.content; -// try std.testing.expectEqual(Content{ .flex_var = null }, ext_content); -// } - -// test "unify - record mismatch on shared field (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const int = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); - -// const field_a = try env.mkRecordField("x", str); -// const field_b = try env.mkRecordField("x", int); - -// const a_rec_data = try env.mkRecordOpen(&[_]RecordField{field_a}); -// const a = try env.module_env.types.freshFromContent(a_rec_data.content); - -// const b_rec_data = try env.mkRecordOpen(&[_]RecordField{field_b}); -// const b = try env.module_env.types.freshFromContent(b_rec_data.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// const desc_b = try env.getDescForRootVar(b); -// try std.testing.expectEqual(Content.err, desc_b.content); -// } - -// // unification - structure/structure - records open+closed - -// test "unify - open record extends closed (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const field_x = try env.mkRecordField("field_x", str); -// const field_y = try env.mkRecordField("field_y", str); - -// const open = try env.module_env.types.freshFromContent((try env.mkRecordOpen(&[_]RecordField{ field_x, field_y })).content); -// const closed = try env.module_env.types.freshFromContent((try env.mkRecordClosed(&[_]RecordField{field_x})).content); - -// const result = try env.unify(open, closed); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = closed }, env.module_env.types.getSlot(open)); -// try std.testing.expectEqual(Content.err, (try env.getDescForRootVar(closed)).content); -// } - -// test "unify - closed record extends open" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const field_x = try env.mkRecordField("field_x", str); -// const field_y = try env.mkRecordField("field_y", str); - -// const open = try env.module_env.types.freshFromContent((try env.mkRecordOpen(&[_]RecordField{field_x})).content); -// const closed = try env.module_env.types.freshFromContent((try env.mkRecordClosed(&[_]RecordField{ field_x, field_y })).content); - -// const result = try env.unify(open, closed); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = closed }, env.module_env.types.getSlot(open)); -// } - -// test "unify - open vs closed records with type mismatch (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const int = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); - -// const field_x_str = try env.mkRecordField("field_x_str", str); -// const field_x_int = try env.mkRecordField("field_x_int", int); - -// const open = try env.module_env.types.freshFromContent((try env.mkRecordOpen(&[_]RecordField{field_x_str})).content); -// const closed = try env.module_env.types.freshFromContent((try env.mkRecordClosed(&[_]RecordField{field_x_int})).content); - -// const result = try env.unify(open, closed); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = closed }, env.module_env.types.getSlot(open)); - -// const desc = try env.getDescForRootVar(closed); -// try std.testing.expectEqual(Content.err, desc.content); -// } - -// test "unify - closed vs open records with type mismatch (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const int = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); - -// const field_x_str = try env.mkRecordField("field_x_str", str); -// const field_x_int = try env.mkRecordField("field_x_int", int); - -// const closed = try env.module_env.types.freshFromContent((try env.mkRecordClosed(&[_]RecordField{field_x_int})).content); -// const open = try env.module_env.types.freshFromContent((try env.mkRecordOpen(&[_]RecordField{field_x_str})).content); - -// const result = try env.unify(closed, open); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = open }, env.module_env.types.getSlot(closed)); - -// const desc = try env.getDescForRootVar(open); -// try std.testing.expectEqual(Content.err, desc.content); -// } - -// // unification - tag unions - partition tags - -// test "partitionTags - same tags" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const tag_x = try env.mkTag("X", &[_]Var{@enumFromInt(0)}); -// const tag_y = try env.mkTag("Y", &[_]Var{@enumFromInt(1)}); - -// const range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ tag_x, tag_y }); - -// const result = try partitionTags(&env.module_env.getIdentStore(), &env.scratch, range, range); - -// try std.testing.expectEqual(0, result.only_in_a.len()); -// try std.testing.expectEqual(0, result.only_in_b.len()); -// try std.testing.expectEqual(2, result.in_both.len()); - -// const both_slice = env.scratch.in_both_tags.sliceRange(result.in_both); -// try std.testing.expectEqual(tag_x, both_slice[0].a); -// try std.testing.expectEqual(tag_x, both_slice[0].b); -// try std.testing.expectEqual(tag_y, both_slice[1].a); -// try std.testing.expectEqual(tag_y, both_slice[1].b); -// } - -// test "partitionTags - disjoint fields" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a1 = try env.mkTag("A1", &[_]Var{@enumFromInt(0)}); -// const a2 = try env.mkTag("A2", &[_]Var{@enumFromInt(1)}); -// const b1 = try env.mkTag("B1", &[_]Var{@enumFromInt(2)}); - -// const a_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ a1, a2 }); -// const b_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{b1}); - -// const result = try partitionTags(&env.module_env.getIdentStore(), &env.scratch, a_range, b_range); - -// try std.testing.expectEqual(2, result.only_in_a.len()); -// try std.testing.expectEqual(1, result.only_in_b.len()); -// try std.testing.expectEqual(0, result.in_both.len()); - -// const only_in_a_slice = env.scratch.only_in_a_tags.sliceRange(result.only_in_a); -// try std.testing.expectEqual(a1, only_in_a_slice[0]); -// try std.testing.expectEqual(a2, only_in_a_slice[1]); - -// const only_in_b_slice = env.scratch.only_in_b_tags.sliceRange(result.only_in_b); -// try std.testing.expectEqual(b1, only_in_b_slice[0]); -// } - -// test "partitionTags - overlapping tags" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const a1 = try env.mkTag("A", &[_]Var{@enumFromInt(0)}); -// const both = try env.mkTag("Both", &[_]Var{@enumFromInt(1)}); -// const b1 = try env.mkTag("B", &[_]Var{@enumFromInt(2)}); - -// const a_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ a1, both }); -// const b_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ b1, both }); - -// const result = try partitionTags(&env.module_env.getIdentStore(), &env.scratch, a_range, b_range); - -// try std.testing.expectEqual(1, result.only_in_a.len()); -// try std.testing.expectEqual(1, result.only_in_b.len()); -// try std.testing.expectEqual(1, result.in_both.len()); - -// const both_slice = env.scratch.in_both_tags.sliceRange(result.in_both); -// try std.testing.expectEqual(both, both_slice[0].a); -// try std.testing.expectEqual(both, both_slice[0].b); - -// const only_in_a_slice = env.scratch.only_in_a_tags.sliceRange(result.only_in_a); -// try std.testing.expectEqual(a1, only_in_a_slice[0]); - -// const only_in_b_slice = env.scratch.only_in_b_tags.sliceRange(result.only_in_b); -// try std.testing.expectEqual(b1, only_in_b_slice[0]); -// } - -// test "partitionTags - reordering is normalized" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const f1 = try env.mkTag("F1", &[_]Var{@enumFromInt(0)}); -// const f2 = try env.mkTag("F2", &[_]Var{@enumFromInt(1)}); -// const f3 = try env.mkTag("F3", &[_]Var{@enumFromInt(2)}); - -// const a_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ f3, f1, f2 }); -// const b_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ f1, f2, f3 }); - -// const result = try partitionTags(&env.module_env.getIdentStore(), &env.scratch, a_range, b_range); - -// try std.testing.expectEqual(0, result.only_in_a.len()); -// try std.testing.expectEqual(0, result.only_in_b.len()); -// try std.testing.expectEqual(3, result.in_both.len()); - -// const both_slice = env.scratch.in_both_tags.sliceRange(result.in_both); -// try std.testing.expectEqual(f1, both_slice[0].a); -// try std.testing.expectEqual(f1, both_slice[0].b); -// try std.testing.expectEqual(f2, both_slice[1].a); -// try std.testing.expectEqual(f2, both_slice[1].b); -// try std.testing.expectEqual(f3, both_slice[2].a); -// try std.testing.expectEqual(f3, both_slice[2].b); -// } - -// // unification - structure/structure - tag unions closed - -// test "unify - identical closed tag_unions" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const tag = try env.mkTag("A", &[_]Var{str}); -// const tags = [_]Tag{tag}; -// const tag_union_data = try env.mkTagUnionClosed(&tags); - -// const a = try env.module_env.types.freshFromContent(tag_union_data.content); -// const b = try env.module_env.types.freshFromContent(tag_union_data.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// const b_tag_union = try TestEnv.getTagUnionOrErr(try env.getDescForRootVar(b)); -// const b_tags = env.module_env.types.tags.sliceRange(b_tag_union.tags); -// const b_tags_names = b_tags.items(.name); -// const b_tags_args = b_tags.items(.args); -// try std.testing.expectEqual(1, b_tags.len); -// try std.testing.expectEqual(tag.name, b_tags_names[0]); -// try std.testing.expectEqual(tag.args, b_tags_args[0]); - -// try std.testing.expectEqual(1, b_tags.len); - -// const b_tag_args = env.module_env.types.vars.sliceRange(b_tags_args[0]); -// try std.testing.expectEqual(1, b_tag_args.len); -// try std.testing.expectEqual(str, b_tag_args[0]); -// } - -// test "unify - closed tag_unions with diff args (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const int = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); - -// const a_tag = try env.mkTag("A", &[_]Var{str}); -// const a_tags = [_]Tag{a_tag}; -// const a_tag_union_data = try env.mkTagUnionClosed(&a_tags); -// const a = try env.module_env.types.freshFromContent(a_tag_union_data.content); - -// const b_tag = try env.mkTag("A", &[_]Var{int}); -// const b_tags = [_]Tag{b_tag}; -// const b_tag_union_data = try env.mkTagUnionClosed(&b_tags); -// const b = try env.module_env.types.freshFromContent(b_tag_union_data.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// const desc = try env.getDescForRootVar(b); -// try std.testing.expectEqual(Content.err, desc.content); -// } - -// // unification - structure/structure - tag unions open - -// test "unify - identical open tag unions" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const tag_shared = try env.mkTag("Shared", &[_]Var{ str, str }); - -// const tag_union_a = try env.mkTagUnionOpen(&[_]Tag{tag_shared}); -// const a = try env.module_env.types.freshFromContent(tag_union_a.content); - -// const tag_union_b = try env.mkTagUnionOpen(&[_]Tag{tag_shared}); -// const b = try env.module_env.types.freshFromContent(tag_union_b.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// // check that the update var at b is correct - -// const b_tag_union = try TestEnv.getTagUnionOrErr(try env.getDescForRootVar(b)); -// try std.testing.expectEqual(1, b_tag_union.tags.len()); - -// const b_tags = env.module_env.types.tags.sliceRange(b_tag_union.tags); -// const b_tags_names = b_tags.items(.name); -// const b_tags_args = b_tags.items(.args); -// try std.testing.expectEqual(1, b_tags.len); -// try std.testing.expectEqual(tag_shared.name, b_tags_names[0]); -// try std.testing.expectEqual(tag_shared.args, b_tags_args[0]); - -// const b_ext = env.module_env.types.resolveVar(b_tag_union.ext).desc.content; -// try std.testing.expectEqual(Content{ .flex_var = null }, b_ext); - -// // check that fresh vars are correct - -// try std.testing.expectEqual(0, env.scratch.fresh_vars.len()); -// } - -// test "unify - open tag union a extends b" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const int = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); - -// const tag_a_only = try env.mkTag("A", &[_]Var{str}); -// const tag_shared = try env.mkTag("Shared", &[_]Var{ int, int }); - -// const tag_union_a = try env.mkTagUnionOpen(&[_]Tag{ tag_a_only, tag_shared }); -// const a = try env.module_env.types.freshFromContent(tag_union_a.content); - -// const tag_union_b = try env.mkTagUnionOpen(&[_]Tag{tag_shared}); -// const b = try env.module_env.types.freshFromContent(tag_union_b.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// // check that the update var at b is correct - -// const b_tag_union = try TestEnv.getTagUnionOrErr(try env.getDescForRootVar(b)); -// try std.testing.expectEqual(1, b_tag_union.tags.len()); - -// const b_tags = env.module_env.types.tags.sliceRange(b_tag_union.tags); -// const b_tags_names = b_tags.items(.name); -// const b_tags_args = b_tags.items(.args); -// try std.testing.expectEqual(1, b_tags.len); -// try std.testing.expectEqual(tag_shared.name, b_tags_names[0]); -// try std.testing.expectEqual(tag_shared.args, b_tags_args[0]); - -// const b_ext_tag_union = try TestEnv.getTagUnionOrErr(env.module_env.types.resolveVar(b_tag_union.ext).desc); -// try std.testing.expectEqual(1, b_ext_tag_union.tags.len()); - -// const b_ext_tags = env.module_env.types.tags.sliceRange(b_ext_tag_union.tags); -// const b_ext_tags_names = b_ext_tags.items(.name); -// const b_ext_tags_args = b_ext_tags.items(.args); -// try std.testing.expectEqual(1, b_ext_tags.len); -// try std.testing.expectEqual(tag_a_only.name, b_ext_tags_names[0]); -// try std.testing.expectEqual(tag_a_only.args, b_ext_tags_args[0]); - -// const b_ext_ext = env.module_env.types.resolveVar(b_ext_tag_union.ext).desc.content; -// try std.testing.expectEqual(Content{ .flex_var = null }, b_ext_ext); - -// // check that fresh vars are correct - -// try std.testing.expectEqual(1, env.scratch.fresh_vars.len()); -// try std.testing.expectEqual(b_tag_union.ext, env.scratch.fresh_vars.items.items[0]); -// } - -// test "unify - open tag union b extends a" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const int = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); - -// const tag_b_only = try env.mkTag("A", &[_]Var{ str, int }); -// const tag_shared = try env.mkTag("Shared", &[_]Var{int}); - -// const tag_union_a = try env.mkTagUnionOpen(&[_]Tag{tag_shared}); -// const a = try env.module_env.types.freshFromContent(tag_union_a.content); - -// const tag_union_b = try env.mkTagUnionOpen(&[_]Tag{ tag_b_only, tag_shared }); -// const b = try env.module_env.types.freshFromContent(tag_union_b.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// // check that the update var at b is correct - -// const b_tag_union = try TestEnv.getTagUnionOrErr(try env.getDescForRootVar(b)); -// try std.testing.expectEqual(1, b_tag_union.tags.len()); - -// const b_tags = env.module_env.types.tags.sliceRange(b_tag_union.tags); -// const b_tags_names = b_tags.items(.name); -// const b_tags_args = b_tags.items(.args); -// try std.testing.expectEqual(1, b_tags.len); -// try std.testing.expectEqual(tag_shared.name, b_tags_names[0]); -// try std.testing.expectEqual(tag_shared.args, b_tags_args[0]); - -// const b_ext_tag_union = try TestEnv.getTagUnionOrErr(env.module_env.types.resolveVar(b_tag_union.ext).desc); -// try std.testing.expectEqual(1, b_ext_tag_union.tags.len()); - -// const b_ext_tags = env.module_env.types.tags.sliceRange(b_ext_tag_union.tags); -// const b_ext_tags_names = b_ext_tags.items(.name); -// const b_ext_tags_args = b_ext_tags.items(.args); -// try std.testing.expectEqual(1, b_ext_tags.len); -// try std.testing.expectEqual(tag_b_only.name, b_ext_tags_names[0]); -// try std.testing.expectEqual(tag_b_only.args, b_ext_tags_args[0]); - -// const b_ext_ext = env.module_env.types.resolveVar(b_ext_tag_union.ext).desc.content; -// try std.testing.expectEqual(Content{ .flex_var = null }, b_ext_ext); - -// // check that fresh vars are correct - -// try std.testing.expectEqual(1, env.scratch.fresh_vars.len()); -// try std.testing.expectEqual(b_tag_union.ext, env.scratch.fresh_vars.items.items[0]); -// } - -// test "unify - both extend open tag union" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const int = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); -// const bool_ = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i8 } }); - -// const tag_a_only = try env.mkTag("A", &[_]Var{bool_}); -// const tag_b_only = try env.mkTag("B", &[_]Var{ str, int }); -// const tag_shared = try env.mkTag("Shared", &[_]Var{int}); - -// const tag_union_a = try env.mkTagUnionOpen(&[_]Tag{ tag_a_only, tag_shared }); -// const a = try env.module_env.types.freshFromContent(tag_union_a.content); - -// const tag_union_b = try env.mkTagUnionOpen(&[_]Tag{ tag_b_only, tag_shared }); -// const b = try env.module_env.types.freshFromContent(tag_union_b.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// // check that the update var at b is correct - -// const b_tag_union = try TestEnv.getTagUnionOrErr(try env.getDescForRootVar(b)); -// try std.testing.expectEqual(3, b_tag_union.tags.len()); - -// const b_tags = env.module_env.types.tags.sliceRange(b_tag_union.tags); -// try std.testing.expectEqual(3, b_tags.len); -// try std.testing.expectEqual(tag_shared, b_tags.get(0)); -// try std.testing.expectEqual(tag_a_only, b_tags.get(1)); -// try std.testing.expectEqual(tag_b_only, b_tags.get(2)); - -// const b_ext = env.module_env.types.resolveVar(b_tag_union.ext).desc.content; -// try std.testing.expectEqual(Content{ .flex_var = null }, b_ext); - -// // check that fresh vars are correct - -// try std.testing.expectEqual(3, env.scratch.fresh_vars.len()); - -// const only_a_var = env.scratch.fresh_vars.get(@enumFromInt(0)).*; -// const only_a_tag_union = try TestEnv.getTagUnionOrErr(env.module_env.types.resolveVar(only_a_var).desc); -// try std.testing.expectEqual(1, only_a_tag_union.tags.len()); -// const only_a_tags = env.module_env.types.getTagsSlice(only_a_tag_union.tags); -// try std.testing.expectEqual(tag_a_only, only_a_tags.get(0)); - -// const only_b_var = env.scratch.fresh_vars.get(@enumFromInt(1)).*; -// const only_b_tag_union = try TestEnv.getTagUnionOrErr(env.module_env.types.resolveVar(only_b_var).desc); -// try std.testing.expectEqual(1, only_b_tag_union.tags.len()); -// const only_b_tags = env.module_env.types.getTagsSlice(only_b_tag_union.tags); -// try std.testing.expectEqual(tag_b_only, only_b_tags.get(0)); - -// const ext_var = env.scratch.fresh_vars.get(@enumFromInt(2)).*; -// const ext_content = env.module_env.types.resolveVar(ext_var).desc.content; -// try std.testing.expectEqual(Content{ .flex_var = null }, ext_content); -// } - -// test "unify - open tag unions a & b have same tag name with diff args (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const int = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }); - -// const tag_a_only = try env.mkTag("A", &[_]Var{str}); -// const tag_shared = try env.mkTag("A", &[_]Var{ int, int }); - -// const tag_union_a = try env.mkTagUnionOpen(&[_]Tag{ tag_a_only, tag_shared }); -// const a = try env.module_env.types.freshFromContent(tag_union_a.content); - -// const tag_union_b = try env.mkTagUnionOpen(&[_]Tag{tag_shared}); -// const b = try env.module_env.types.freshFromContent(tag_union_b.content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// const desc = try env.getDescForRootVar(b); -// try std.testing.expectEqual(Content.err, desc.content); -// } - -// // unification - structure/structure - records open+closed - -// test "unify - open tag extends closed (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const tag_shared = try env.mkTag("Shared", &[_]Var{str}); -// const tag_a_only = try env.mkTag("A", &[_]Var{str}); - -// const a = try env.module_env.types.freshFromContent((try env.mkTagUnionOpen(&[_]Tag{ tag_shared, tag_a_only })).content); -// const b = try env.module_env.types.freshFromContent((try env.mkTagUnionClosed(&[_]Tag{tag_shared})).content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); -// try std.testing.expectEqual(Content.err, (try env.getDescForRootVar(b)).content); -// } - -// test "unify - closed tag union extends open" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const tag_shared = try env.mkTag("Shared", &[_]Var{str}); -// const tag_b_only = try env.mkTag("B", &[_]Var{str}); - -// const a = try env.module_env.types.freshFromContent((try env.mkTagUnionOpen(&[_]Tag{tag_shared})).content); -// const b = try env.module_env.types.freshFromContent((try env.mkTagUnionClosed(&[_]Tag{ tag_shared, tag_b_only })).content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(.ok, result); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// // check that the update var at b is correct - -// const b_tag_union = try TestEnv.getTagUnionOrErr(try env.getDescForRootVar(b)); -// try std.testing.expectEqual(1, b_tag_union.tags.len()); - -// const b_tags = env.module_env.types.tags.sliceRange(b_tag_union.tags); -// const b_tags_names = b_tags.items(.name); -// const b_tags_args = b_tags.items(.args); -// try std.testing.expectEqual(1, b_tags.len); -// try std.testing.expectEqual(tag_shared.name, b_tags_names[0]); -// try std.testing.expectEqual(tag_shared.args, b_tags_args[0]); - -// const b_ext_tag_union = try TestEnv.getTagUnionOrErr(env.module_env.types.resolveVar(b_tag_union.ext).desc); -// try std.testing.expectEqual(1, b_ext_tag_union.tags.len()); - -// const b_ext_tags = env.module_env.types.tags.sliceRange(b_ext_tag_union.tags); -// const b_ext_tags_names = b_ext_tags.items(.name); -// const b_ext_tags_args = b_ext_tags.items(.args); -// try std.testing.expectEqual(1, b_ext_tags.len); -// try std.testing.expectEqual(tag_b_only.name, b_ext_tags_names[0]); -// try std.testing.expectEqual(tag_b_only.args, b_ext_tags_args[0]); - -// const b_ext_ext = env.module_env.types.resolveVar(b_ext_tag_union.ext).desc.content; -// try std.testing.expectEqual(Content{ .structure = .empty_tag_union }, b_ext_ext); - -// // check that fresh vars are correct - -// try std.testing.expectEqual(1, env.scratch.fresh_vars.len()); -// try std.testing.expectEqual(b_tag_union.ext, env.scratch.fresh_vars.items.items[0]); -// } - -// test "unify - open vs closed tag union with type mismatch (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const bool_ = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i8 } }); - -// const tag_a = try env.mkTag("A", &[_]Var{str}); -// const tag_b = try env.mkTag("A", &[_]Var{bool_}); - -// const a = try env.module_env.types.freshFromContent((try env.mkTagUnionOpen(&[_]Tag{tag_a})).content); -// const b = try env.module_env.types.freshFromContent((try env.mkTagUnionClosed(&[_]Tag{tag_b})).content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// const desc = try env.getDescForRootVar(b); -// try std.testing.expectEqual(Content.err, desc.content); -// } - -// test "unify - closed vs open tag union with type mismatch (fail)" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); -// const bool_ = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i8 } }); - -// const tag_a = try env.mkTag("A", &[_]Var{str}); -// const tag_b = try env.mkTag("B", &[_]Var{bool_}); - -// const a = try env.module_env.types.freshFromContent((try env.mkTagUnionClosed(&[_]Tag{tag_a})).content); -// const b = try env.module_env.types.freshFromContent((try env.mkTagUnionOpen(&[_]Tag{tag_b})).content); - -// const result = try env.unify(a, b); - -// try std.testing.expectEqual(false, result.isOk()); -// try std.testing.expectEqual(Slot{ .redirect = b }, env.module_env.types.getSlot(a)); - -// const desc = try env.getDescForRootVar(b); -// try std.testing.expectEqual(Content.err, desc.content); -// } - -// // unification - recursion - -// test "unify - fails on infinite type" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const str_var = try env.module_env.types.freshFromContent(Content{ .structure = .str }); - -// const a = try env.module_env.types.fresh(); -// const a_elems_range = try env.module_env.types.appendVars(&[_]Var{ a, str_var }); -// const a_tuple = types_mod.Tuple{ .elems = a_elems_range }; -// try env.module_env.types.setRootVarContent(a, Content{ .structure = .{ .tuple = a_tuple } }); - -// const b = try env.module_env.types.fresh(); -// const b_elems_range = try env.module_env.types.appendVars(&[_]Var{ b, str_var }); -// const b_tuple = types_mod.Tuple{ .elems = b_elems_range }; -// try env.module_env.types.setRootVarContent(b, Content{ .structure = .{ .tuple = b_tuple } }); - -// const result = try env.unify(a, b); - -// switch (result) { -// .ok => try std.testing.expect(false), -// .problem => |problem_idx| { -// const problem = env.problems.problems.get(problem_idx); -// try std.testing.expectEqual(.infinite_recursion, @as(Problem.Tag, problem)); -// }, -// } -// } - -// test "unify - fails on anonymous recursion" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// const list_var_a = try env.module_env.types.fresh(); -// const list_content_a = Content{ -// .structure = .{ .list = list_var_a }, -// }; -// try env.module_env.types.setRootVarContent(list_var_a, list_content_a); - -// const list_var_b = try env.module_env.types.fresh(); -// const list_content_b = Content{ -// .structure = .{ .list = list_var_b }, -// }; -// try env.module_env.types.setRootVarContent(list_var_b, list_content_b); - -// const result = try env.unify(list_var_a, list_var_b); - -// switch (result) { -// .ok => try std.testing.expect(false), -// .problem => |problem_idx| { -// const problem = env.problems.problems.get(problem_idx); -// try std.testing.expectEqual(.anonymous_recursion, @as(Problem.Tag, problem)); -// }, -// } -// } - -// test "unify - succeeds on nominal, tag union recursion" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// var types_store = &env.module_env.types; - -// // Create vars in the required order for adjacency to work out -// const a = try types_store.fresh(); -// const b = try types_store.fresh(); -// const elem = try types_store.fresh(); -// const ext = try types_store.fresh(); - -// // Create the tag union content that references type_a_nominal -// const a_cons_tag = try env.mkTag("Cons", &[_]Var{ elem, a }); -// const a_nil_tag = try env.mkTag("Nil", &[_]Var{}); -// const a_backing = try types_store.freshFromContent(try types_store.mkTagUnion(&.{ a_cons_tag, a_nil_tag }, ext)); -// try types_store.setVarContent(a, try env.mkNominalType("TypeA", a_backing, &.{})); - -// const b_cons_tag = try env.mkTag("Cons", &[_]Var{ elem, b }); -// const b_nil_tag = try env.mkTag("Nil", &[_]Var{}); -// const b_backing = try types_store.freshFromContent(try types_store.mkTagUnion(&.{ b_cons_tag, b_nil_tag }, ext)); -// try types_store.setVarContent(b, try env.mkNominalType("TypeA", b_backing, &.{})); - -// const result_nominal_type = try env.unify(a, b); -// try std.testing.expectEqual(.ok, result_nominal_type); - -// const result_tag_union = try env.unify(a_backing, b_backing); -// try std.testing.expectEqual(.ok, result_tag_union); -// } - -// test "integer literal 255 fits in U8" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a literal with value 255 (8 bits unsigned) -// const literal_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"8"), -// }; -// const literal_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = literal_requirements } } }); - -// // Create U8 type -// const u8_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .int = .u8 } } } }); - -// // They should unify successfully -// const result = try env.unify(literal_var, u8_var); -// try std.testing.expect(result == .ok); -// } - -// test "integer literal 256 does not fit in U8" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a literal with value 256 (9 bits, no sign) -// const literal_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"9_to_15"), -// }; -// const literal_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = literal_requirements } } }); - -// // Create U8 type -// const u8_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .int = .u8 } } } }); - -// // They should NOT unify - type mismatch expected -// const result = try env.unify(literal_var, u8_var); -// try std.testing.expect(result == .problem); -// } - -// test "integer literal -128 fits in I8" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a literal with value -128 (needs sign, 7 bits after adjustment) -// const literal_requirements = Num.IntRequirements{ -// .sign_needed = true, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), -// }; -// const literal_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = literal_requirements } } }); - -// // Create I8 type -// const i8_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .int = .i8 } } } }); - -// // They should unify successfully -// const result = try env.unify(literal_var, i8_var); -// try std.testing.expect(result == .ok); -// } - -// test "integer literal -129 does not fit in I8" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a literal with value -129 (needs sign, 8 bits) -// const literal_requirements = Num.IntRequirements{ -// .sign_needed = true, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"8"), -// }; -// const literal_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = literal_requirements } } }); - -// // Create I8 type -// const i8_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .int = .i8 } } } }); - -// // They should NOT unify - type mismatch expected -// const result = try env.unify(literal_var, i8_var); -// try std.testing.expect(result == .problem); -// } - -// test "negative literal cannot unify with unsigned type" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a literal with negative value (sign needed) -// const literal_requirements = Num.IntRequirements{ -// .sign_needed = true, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), -// }; -// const literal_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = literal_requirements } } }); - -// // Create U8 type -// const u8_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .int = .u8 } } } }); - -// // They should NOT unify - type mismatch expected -// const result = try env.unify(literal_var, u8_var); -// try std.testing.expect(result == .problem); -// } - -// test "float literal that fits in F32" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a literal that fits in F32 -// const literal_requirements = Num.FracRequirements{ -// .fits_in_f32 = true, -// .fits_in_dec = true, -// }; -// const literal_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_unbound = literal_requirements } } }); - -// // Create F32 type -// const f32_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .frac = .f32 } } } }); - -// // They should unify successfully -// const result = try env.unify(literal_var, f32_var); -// try std.testing.expect(result == .ok); -// } - -// test "float literal that doesn't fit in F32" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a literal that doesn't fit in F32 -// const literal_requirements = Num.FracRequirements{ -// .fits_in_f32 = false, -// .fits_in_dec = true, -// }; -// const literal_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_unbound = literal_requirements } } }); - -// // Create F32 type -// const f32_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .frac = .f32 } } } }); - -// // They should NOT unify - type mismatch expected -// const result = try env.unify(literal_var, f32_var); -// try std.testing.expect(result == .problem); -// } - -// test "float literal NaN doesn't fit in Dec" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a literal like NaN that doesn't fit in Dec -// const literal_requirements = Num.FracRequirements{ -// .fits_in_f32 = true, -// .fits_in_dec = false, -// }; -// const literal_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_unbound = literal_requirements } } }); - -// // Create Dec type -// const dec_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_compact = .{ .frac = .dec } } } }); - -// // They should NOT unify - type mismatch expected -// const result = try env.unify(literal_var, dec_var); -// try std.testing.expect(result == .problem); -// } - -// test "two integer literals with different requirements unify to most restrictive" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a literal with value 100 (7 bits, no sign) -// const literal1_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), -// }; -// const literal1_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = literal1_requirements } } }); - -// // Create a literal with value 200 (8 bits, no sign) -// const literal2_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"8"), -// }; -// const literal2_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = literal2_requirements } } }); - -// // They should unify successfully -// const result = try env.unify(literal1_var, literal2_var); -// try std.testing.expect(result == .ok); -// } - -// test "positive and negative literals unify with sign requirement" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create an unsigned literal -// const literal1_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), -// }; -// const literal1_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = literal1_requirements } } }); - -// // Create a signed literal -// const literal2_requirements = Num.IntRequirements{ -// .sign_needed = true, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), -// }; -// const literal2_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = literal2_requirements } } }); - -// // They should unify successfully (creating a signed type that can hold both) -// const result = try env.unify(literal1_var, literal2_var); -// try std.testing.expect(result == .ok); -// } - -// test "unify - num_unbound with frac_unbound" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a num_unbound (like literal 1) -// const num_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), -// }; -// const num_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = num_requirements } } }); - -// // Create a frac_unbound (like literal 2.5) -// const frac_requirements = Num.FracRequirements{ -// .fits_in_f32 = true, -// .fits_in_dec = true, -// }; -// const frac_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_unbound = frac_requirements } } }); - -// // They should unify successfully with frac_unbound winning -// const result = try env.unify(num_var, frac_var); -// try std.testing.expect(result == .ok); - -// // Check that the result is frac_unbound -// const resolved = env.module_env.types.resolveVar(num_var); -// switch (resolved.desc.content) { -// .structure => |structure| { -// switch (structure) { -// .num => |num| { -// switch (num) { -// .frac_unbound => {}, // Expected -// else => return error.ExpectedFracUnbound, -// } -// }, -// else => return error.ExpectedNum, -// } -// }, -// else => return error.ExpectedStructure, -// } -// } - -// test "unify - frac_unbound with num_unbound (reverse order)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a frac_unbound (like literal 2.5) -// const frac_requirements = Num.FracRequirements{ -// .fits_in_f32 = true, -// .fits_in_dec = true, -// }; -// const frac_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_unbound = frac_requirements } } }); - -// // Create a num_unbound (like literal 1) -// const num_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), -// }; -// const num_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = num_requirements } } }); - -// // They should unify successfully with frac_unbound winning -// const result = try env.unify(frac_var, num_var); -// try std.testing.expect(result == .ok); - -// // Check that the result is frac_unbound -// const resolved = env.module_env.types.resolveVar(frac_var); -// switch (resolved.desc.content) { -// .structure => |structure| { -// switch (structure) { -// .num => |num| { -// switch (num) { -// .frac_unbound => {}, // Expected -// else => return error.ExpectedFracUnbound, -// } -// }, -// else => return error.ExpectedNum, -// } -// }, -// else => return error.ExpectedStructure, -// } -// } - -// test "unify - int_unbound with num_unbound" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create an int_unbound (like literal -5) -// const int_requirements = Num.IntRequirements{ -// .sign_needed = true, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), -// }; -// const int_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_unbound = int_requirements } } }); - -// // Create a num_unbound (like literal 1) -// const num_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), -// }; -// const num_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = num_requirements } } }); - -// // They should unify successfully with int_unbound winning -// const result = try env.unify(int_var, num_var); -// try std.testing.expect(result == .ok); - -// // Check that the result is int_unbound -// const resolved = env.module_env.types.resolveVar(int_var); -// switch (resolved.desc.content) { -// .structure => |structure| { -// switch (structure) { -// .num => |num| { -// switch (num) { -// .int_unbound => |requirements| { -// // Should have merged the requirements - sign_needed should be true -// try std.testing.expect(requirements.sign_needed == true); -// }, -// else => return error.ExpectedIntUnbound, -// } -// }, -// else => return error.ExpectedNum, -// } -// }, -// else => return error.ExpectedStructure, -// } -// } - -// test "unify - num_unbound with int_unbound (reverse order)" { -// const gpa = std.testing.allocator; - -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a num_unbound (like literal 1) -// const num_requirements = Num.IntRequirements{ -// .sign_needed = false, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), -// }; -// const num_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = num_requirements } } }); - -// // Create an int_unbound (like literal -5) -// const int_requirements = Num.IntRequirements{ -// .sign_needed = true, -// .bits_needed = @intFromEnum(Num.Int.BitsNeeded.@"7"), -// }; -// const int_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_unbound = int_requirements } } }); - -// // They should unify successfully with int_unbound winning -// const result = try env.unify(num_var, int_var); -// try std.testing.expect(result == .ok); - -// // Check that the result is int_unbound -// const resolved = env.module_env.types.resolveVar(num_var); -// switch (resolved.desc.content) { -// .structure => |structure| { -// switch (structure) { -// .num => |num| { -// switch (num) { -// .int_unbound => |requirements| { -// // Should have merged the requirements - sign_needed should be true -// try std.testing.expect(requirements.sign_needed == true); -// }, -// else => return error.ExpectedIntUnbound, -// } -// }, -// else => return error.ExpectedNum, -// } -// }, -// else => return error.ExpectedStructure, -// } -// } - -// test "heterogeneous list reports only first incompatibility" { -// const gpa = std.testing.allocator; -// var env = try TestEnv.init(gpa); -// defer env.deinit(); - -// // Create a list type with three different elements -// const num_var = try env.module_env.types.freshFromContent(.{ .structure = .{ .num = .{ .int_unbound = .{ .sign_needed = false, .bits_needed = 7 } } } }); -// const str_var = try env.module_env.types.freshFromContent(.{ .structure = .str }); -// const frac_var = try env.module_env.types.freshFromContent(.{ .structure = .{ .num = .{ .frac_unbound = .{ .fits_in_f32 = true, .fits_in_dec = true } } } }); - -// // Create a list element type variable -// const elem_var = try env.module_env.types.fresh(); - -// // Unify first element (number) with elem_var - should succeed -// const result1 = try env.unify(elem_var, num_var); -// try std.testing.expectEqual(.ok, result1); - -// // Unify second element (string) with elem_var - should fail -// const result2 = try env.unify(elem_var, str_var); -// try std.testing.expectEqual(false, result2.isOk()); - -// // Unify third element (fraction) with elem_var - should succeed (int can be promoted to frac) -// const result3 = try env.unify(elem_var, frac_var); -// try std.testing.expectEqual(.ok, result3); - -// // Check that we have exactly one problem recorded (from the string unification) -// try std.testing.expect(env.problems.problems.len() == 1); -// } +} diff --git a/src/cli/CliContext.zig b/src/cli/CliContext.zig new file mode 100644 index 0000000000..fd27f53937 --- /dev/null +++ b/src/cli/CliContext.zig @@ -0,0 +1,385 @@ +//! CLI Context +//! +//! Provides shared context for CLI operations including allocators and error +//! accumulation. This enables: +//! - Consistent resource management across all CLI commands +//! - Structured error reporting with the Report system +//! - Testable CLI code by capturing output with custom writers +//! +//! The key design principle is that `error.CliError` is the ONLY error type +//! that CLI functions should return. This ensures: +//! - Every error is properly reported (no silent failures) +//! - Consistent error formatting across all commands +//! - The type system enforces proper error handling +//! +//! Usage: +//! fn doSomething(ctx: *CliContext, path: []const u8) CliError!void { +//! const source = std.fs.cwd().readFileAlloc(ctx.gpa, path, ...) catch |err| { +//! return ctx.fail(.{ .file_not_found = .{ .path = path } }); +//! }; +//! defer ctx.gpa.free(source); +//! // Use ctx.arena for temporary allocations... +//! } +//! +//! // At top level: +//! var io = Io.init(); +//! var ctx = CliContext.init(gpa, arena, &io, .build); +//! ctx.initIo(); // Initialize I/O writers after ctx is at its final location +//! defer ctx.deinit(); +//! +//! doSomething(&ctx, "app.roc") catch |err| switch (err) { +//! error.CliError => {}, // Problems already recorded +//! }; +//! +//! try ctx.renderProblemsTo(ctx.io.stderr()); +//! return ctx.exitCode(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const reporting = @import("reporting"); +const problem_mod = @import("CliProblem.zig"); + +const CliProblem = problem_mod.CliProblem; +const Report = reporting.Report; +const Severity = reporting.Severity; +const ColorPalette = reporting.ColorPalette; +const ReportingConfig = reporting.ReportingConfig; + +/// I/O interface for CLI operations. +/// Wraps stdout/stderr with buffered writers. When Zig's std.Io interface +/// becomes available, this struct will be replaced with std.Io. +pub const Io = struct { + stdout_writer: std.fs.File.Writer, + stderr_writer: std.fs.File.Writer, + stdout_buffer: [4096]u8, + stderr_buffer: [4096]u8, + + const Self = @This(); + + /// Create an uninitialized Io struct. + /// MUST call initWriters() after placing the struct at its final location. + pub fn init() Self { + return Self{ + .stdout_writer = undefined, + .stderr_writer = undefined, + .stdout_buffer = undefined, + .stderr_buffer = undefined, + }; + } + + /// Initialize the writers after the struct is at its final memory location. + /// This MUST be called before using stdout() or stderr(). + /// Also enables ANSI escape sequences for colored output. + pub fn initWriters(self: *Self) void { + const stdout_file = std.fs.File.stdout(); + const stderr_file = std.fs.File.stderr(); + + // Enable ANSI escape sequences for colored output (needed on Windows) + _ = stdout_file.getOrEnableAnsiEscapeSupport(); + _ = stderr_file.getOrEnableAnsiEscapeSupport(); + + self.stdout_writer = stdout_file.writer(&self.stdout_buffer); + self.stderr_writer = stderr_file.writer(&self.stderr_buffer); + } + + /// Get the stdout writer interface + pub fn stdout(self: *Self) *std.Io.Writer { + return &self.stdout_writer.interface; + } + + /// Get the stderr writer interface + pub fn stderr(self: *Self) *std.Io.Writer { + return &self.stderr_writer.interface; + } + + /// Flush both stdout and stderr buffers + pub fn flush(self: *Self) void { + self.stdout_writer.interface.flush() catch {}; + self.stderr_writer.interface.flush() catch {}; + } +}; + +/// The single error type for CLI operations. +/// When a function returns this error, it means a problem has been recorded +/// in the CliContext and will be rendered at the top level. +pub const CliError = error{CliError}; + +/// CLI commands that can generate errors +pub const Command = enum { + build, + run, + check, + test_cmd, + dev, + fmt, + bundle, + unbundle, + docs, + repl, + unknown, + + pub fn name(self: Command) []const u8 { + return switch (self) { + .build => "build", + .run => "run", + .check => "check", + .test_cmd => "test", + .dev => "dev", + .fmt => "fmt", + .bundle => "bundle", + .unbundle => "unbundle", + .docs => "docs", + .repl => "repl", + .unknown => "unknown", + }; + } +}; + +/// Shared context for CLI operations. +/// Contains allocators, I/O, and accumulated problems. +pub const CliContext = struct { + /// General purpose allocator for long-lived allocations + gpa: Allocator, + /// Arena allocator for temporary/scoped allocations + arena: Allocator, + /// I/O interface for stdout/stderr + io: *Io, + /// Accumulated problems during CLI operations + problems: std.ArrayList(CliProblem), + /// The CLI command being executed + command: Command, + /// Exit code based on problem severity + exit_code: u8, + + const Self = @This(); + + /// Initialize a new CLI context. + /// After init, call initIo() once the context is at its final memory location. + pub fn init(gpa: Allocator, arena: Allocator, io: *Io, command: Command) Self { + return .{ + .gpa = gpa, + .arena = arena, + .io = io, + .problems = std.ArrayList(CliProblem).empty, + .command = command, + .exit_code = 0, + }; + } + + /// Initialize the I/O writers. Must be called after the context is at its + /// final memory location (i.e., after init() returns and the result is stored). + pub fn initIo(self: *Self) void { + self.io.initWriters(); + } + + /// Clean up resources and flush I/O + pub fn deinit(self: *Self) void { + self.io.flush(); + self.problems.deinit(self.gpa); + } + + /// Add a problem to the context + pub fn addProblem(self: *Self, problem: CliProblem) !void { + try self.problems.append(self.gpa, problem); + + // Update exit code based on severity + const sev = problem.severity(); + switch (sev) { + .fatal => self.exit_code = 1, + .runtime_error => if (self.exit_code == 0) { + self.exit_code = 1; + }, + .warning, .info => {}, + } + } + + /// Add a problem, ignoring allocation failures (for use in error paths) + pub fn addProblemIgnoreError(self: *Self, problem: CliProblem) void { + self.addProblem(problem) catch {}; + } + + /// Add a problem and return CliError. + /// This is the primary way to report errors - it ensures every error + /// is properly recorded before the function returns. + /// + /// Usage: + /// const file = std.fs.cwd().openFile(path, .{}) catch |err| { + /// return ctx.fail(.{ .file_not_found = .{ .path = path } }); + /// }; + pub fn fail(self: *Self, problem: CliProblem) CliError { + self.addProblemIgnoreError(problem); + return error.CliError; + } + + /// Check if any problems have been recorded + pub fn hasProblems(self: *const Self) bool { + return self.problems.items.len > 0; + } + + /// Check if any errors (not just warnings) have been recorded + pub fn hasErrors(self: *const Self) bool { + for (self.problems.items) |problem| { + const sev = problem.severity(); + if (sev == .fatal or sev == .runtime_error) { + return true; + } + } + return false; + } + + /// Get the number of problems + pub fn problemCount(self: *const Self) usize { + return self.problems.items.len; + } + + /// Get the number of errors (fatal + runtime_error) + pub fn errorCount(self: *const Self) usize { + var count: usize = 0; + for (self.problems.items) |problem| { + const sev = problem.severity(); + if (sev == .fatal or sev == .runtime_error) { + count += 1; + } + } + return count; + } + + /// Get the number of warnings + pub fn warningCount(self: *const Self) usize { + var count: usize = 0; + for (self.problems.items) |problem| { + if (problem.severity() == .warning) { + count += 1; + } + } + return count; + } + + /// Render all problems to a writer + pub fn renderProblemsTo(self: *Self, writer: anytype) !void { + const config = ReportingConfig.initColorTerminal(); + + for (self.problems.items) |problem| { + var report = try problem.toReport(self.gpa); + defer report.deinit(); + try reporting.renderReportToTerminal(&report, writer, ColorPalette.ANSI, config); + } + } + + /// Render all problems and return whether there were any errors + pub fn renderAndCheck(self: *Self, writer: anytype) !bool { + try self.renderProblemsTo(writer); + return self.hasErrors(); + } + + /// Clear all problems + pub fn clear(self: *Self) void { + self.problems.clearRetainingCapacity(); + self.exit_code = 0; + } + + /// Get exit code based on recorded problems + pub fn exitCode(self: *const Self) u8 { + return self.exit_code; + } + + // Backward compatibility aliases + pub const suggestedExitCode = exitCode; + pub const renderAll = renderProblemsTo; +}; + +/// Backward compatibility alias +pub const CliErrorContext = CliContext; + +// Helper Functions + +/// Create a context, add a single problem, render it, and return the exit code. +/// Convenience function for simple error cases. +pub fn reportSingleProblem( + allocator: Allocator, + io: *Io, + command: Command, + problem: CliProblem, +) u8 { + var ctx = CliContext.init(allocator, allocator, io, command); + defer ctx.deinit(); + + ctx.addProblemIgnoreError(problem); + ctx.renderProblemsTo(io.stderr()) catch {}; + + return ctx.exitCode(); +} + +/// Render a single problem without creating a full context. +/// Useful for one-off errors that don't need accumulation. +pub fn renderProblem( + allocator: Allocator, + writer: anytype, + problem: CliProblem, +) void { + var report = problem.toReport(allocator) catch return; + defer report.deinit(); + + const config = ReportingConfig.initColorTerminal(); + reporting.renderReportToTerminal(&report, writer, ColorPalette.ANSI, config) catch {}; +} + +// Tests + +test "CliContext accumulates problems" { + const allocator = std.testing.allocator; + var io = Io.init(); + + var ctx = CliContext.init(allocator, allocator, &io, .build); + ctx.initIo(); + defer ctx.deinit(); + + try std.testing.expect(!ctx.hasProblems()); + try std.testing.expect(!ctx.hasErrors()); + try std.testing.expectEqual(@as(usize, 0), ctx.problemCount()); + + try ctx.addProblem(.{ .file_not_found = .{ .path = "app.roc" } }); + + try std.testing.expect(ctx.hasProblems()); + try std.testing.expect(ctx.hasErrors()); + try std.testing.expectEqual(@as(usize, 1), ctx.problemCount()); + try std.testing.expectEqual(@as(u8, 1), ctx.exitCode()); +} + +test "CliContext counts errors vs warnings correctly" { + const allocator = std.testing.allocator; + var io = Io.init(); + + var ctx = CliContext.init(allocator, allocator, &io, .build); + ctx.initIo(); + defer ctx.deinit(); + + try ctx.addProblem(.{ .file_not_found = .{ .path = "a.roc" } }); // fatal + try ctx.addProblem(.{ .file_read_failed = .{ .path = "b.roc", .err = error.OutOfMemory } }); // runtime_error + + try std.testing.expectEqual(@as(usize, 2), ctx.errorCount()); + try std.testing.expectEqual(@as(usize, 0), ctx.warningCount()); +} + +test "CliContext clear resets state" { + const allocator = std.testing.allocator; + var io = Io.init(); + + var ctx = CliContext.init(allocator, allocator, &io, .build); + ctx.initIo(); + defer ctx.deinit(); + + try ctx.addProblem(.{ .file_not_found = .{ .path = "app.roc" } }); + try std.testing.expect(ctx.hasErrors()); + + ctx.clear(); + + try std.testing.expect(!ctx.hasProblems()); + try std.testing.expectEqual(@as(u8, 0), ctx.exitCode()); +} + +test "Command names are correct" { + try std.testing.expectEqualStrings("build", Command.build.name()); + try std.testing.expectEqualStrings("run", Command.run.name()); + try std.testing.expectEqualStrings("test", Command.test_cmd.name()); +} diff --git a/src/cli/CliProblem.zig b/src/cli/CliProblem.zig new file mode 100644 index 0000000000..624ff075de --- /dev/null +++ b/src/cli/CliProblem.zig @@ -0,0 +1,813 @@ +//! CLI Problem Types +//! +//! Structured error types for CLI operations. Each variant contains +//! the context needed to generate a helpful error message. +//! +//! Usage: +//! const problem = CliProblem{ .file_not_found = .{ .path = "app.roc" } }; +//! var report = try problem.toReport(allocator); +//! defer report.deinit(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const reporting = @import("reporting"); +const Report = reporting.Report; +const Severity = reporting.Severity; + +/// Structured CLI errors with context for helpful error messages +pub const CliProblem = union(enum) { + // File I/O Problems + + /// File was not found at the specified path + file_not_found: struct { + path: []const u8, + context: FileContext = .source_file, + }, + + /// Failed to read file contents + file_read_failed: struct { + path: []const u8, + err: anyerror, + }, + + /// Failed to write file + file_write_failed: struct { + path: []const u8, + err: anyerror, + }, + + /// Failed to create directory + directory_create_failed: struct { + path: []const u8, + err: anyerror, + }, + + /// Directory does not exist + directory_not_found: struct { + path: []const u8, + }, + + /// Failed to create temporary directory + temp_dir_failed: struct { + err: anyerror, + }, + + /// Cache directory unavailable + cache_dir_unavailable: struct { + reason: []const u8, + }, + + // Platform Problems + + /// App file doesn't specify a platform + no_platform_found: struct { + app_path: []const u8, + }, + + /// Platform file not found + platform_not_found: struct { + app_path: []const u8, + platform_path: []const u8, + }, + + /// Platform source file missing + platform_source_not_found: struct { + platform_path: []const u8, + searched_paths: []const []const u8, + }, + + /// Missing required module in platform + missing_platform_module: struct { + module_name: []const u8, + platform_path: []const u8, + }, + + /// Type not exposed by module + missing_type_in_module: struct { + module_name: []const u8, + type_name: []const u8, + }, + + /// Circular dependency in platform modules + circular_platform_dependency: struct { + module_chain: []const []const u8, + }, + + /// Platform validation failed (wraps targets_validator result) + platform_validation_failed: struct { + message: []const u8, + }, + + /// Absolute path used for platform (not allowed) + absolute_platform_path: struct { + platform_spec: []const u8, + }, + + // Build/Compilation Problems + + /// Compilation produced errors + compilation_failed: struct { + path: []const u8, + error_count: usize, + }, + + /// Linker failed + linker_failed: struct { + err: anyerror, + target: []const u8, + }, + + /// Object compilation failed + object_compilation_failed: struct { + path: []const u8, + err: anyerror, + }, + + /// Shim generation failed + shim_generation_failed: struct { + err: anyerror, + }, + + /// Entrypoint extraction failed + entrypoint_extraction_failed: struct { + path: []const u8, + reason: []const u8, + }, + + // URL/Download Problems + + /// Invalid URL format + invalid_url: struct { + url: []const u8, + reason: []const u8, + }, + + /// Download failed + download_failed: struct { + url: []const u8, + err: anyerror, + }, + + /// Package not found in cache + package_cache_error: struct { + package: []const u8, + reason: []const u8, + }, + + // Process/Runtime Problems + + /// Child process failed to spawn + child_process_spawn_failed: struct { + command: []const u8, + err: anyerror, + }, + + /// Child process exited with error + child_process_failed: struct { + command: []const u8, + exit_code: u32, + }, + + /// Child process was signaled + child_process_signaled: struct { + command: []const u8, + signal: u32, + }, + + /// Child process wait failed + child_process_wait_failed: struct { + command: []const u8, + err: anyerror, + }, + + /// Shared memory operation failed + shared_memory_failed: struct { + operation: []const u8, + err: anyerror, + }, + + // Module/Header Problems + + /// Expected app header but found something else + expected_app_header: struct { + path: []const u8, + found: []const u8, + }, + + /// Expected platform string in app header + expected_platform_string: struct { + path: []const u8, + }, + + /// Module initialization failed + module_init_failed: struct { + path: []const u8, + err: anyerror, + }, + + /// No exports found in module + no_exports_found: struct { + path: []const u8, + }, + + // Methods + + /// Returns the severity level for this problem + pub fn severity(self: CliProblem) Severity { + return switch (self) { + // Fatal errors - cannot continue + .file_not_found, + .platform_not_found, + .no_platform_found, + .compilation_failed, + .linker_failed, + => .fatal, + + // Runtime errors - operation failed + .file_read_failed, + .file_write_failed, + .directory_create_failed, + .directory_not_found, + .temp_dir_failed, + .cache_dir_unavailable, + .platform_source_not_found, + .missing_platform_module, + .missing_type_in_module, + .circular_platform_dependency, + .platform_validation_failed, + .absolute_platform_path, + .object_compilation_failed, + .shim_generation_failed, + .entrypoint_extraction_failed, + .invalid_url, + .download_failed, + .package_cache_error, + .child_process_spawn_failed, + .child_process_failed, + .child_process_signaled, + .child_process_wait_failed, + .shared_memory_failed, + .expected_app_header, + .expected_platform_string, + .module_init_failed, + .no_exports_found, + => .runtime_error, + }; + } + + /// Generate a Report from this problem + pub fn toReport(self: CliProblem, allocator: Allocator) !Report { + return switch (self) { + .file_not_found => |info| try createFileNotFoundReport(allocator, info), + .file_read_failed => |info| try createFileReadFailedReport(allocator, info), + .file_write_failed => |info| try createFileWriteFailedReport(allocator, info), + .directory_create_failed => |info| try createDirectoryCreateFailedReport(allocator, info), + .directory_not_found => |info| try createDirectoryNotFoundReport(allocator, info), + .temp_dir_failed => |info| try createTempDirFailedReport(allocator, info), + .cache_dir_unavailable => |info| try createCacheDirUnavailableReport(allocator, info), + .no_platform_found => |info| try createNoPlatformFoundReport(allocator, info), + .platform_not_found => |info| try createPlatformNotFoundReport(allocator, info), + .platform_source_not_found => |info| try createPlatformSourceNotFoundReport(allocator, info), + .missing_platform_module => |info| try createMissingPlatformModuleReport(allocator, info), + .missing_type_in_module => |info| try createMissingTypeInModuleReport(allocator, info), + .circular_platform_dependency => |info| try createCircularPlatformDependencyReport(allocator, info), + .platform_validation_failed => |info| try createPlatformValidationFailedReport(allocator, info), + .absolute_platform_path => |info| try createAbsolutePlatformPathReport(allocator, info), + .compilation_failed => |info| try createCompilationFailedReport(allocator, info), + .linker_failed => |info| try createLinkerFailedReport(allocator, info), + .object_compilation_failed => |info| try createObjectCompilationFailedReport(allocator, info), + .shim_generation_failed => |info| try createShimGenerationFailedReport(allocator, info), + .entrypoint_extraction_failed => |info| try createEntrypointExtractionFailedReport(allocator, info), + .invalid_url => |info| try createInvalidUrlReport(allocator, info), + .download_failed => |info| try createDownloadFailedReport(allocator, info), + .package_cache_error => |info| try createPackageCacheErrorReport(allocator, info), + .child_process_spawn_failed => |info| try createChildProcessSpawnFailedReport(allocator, info), + .child_process_failed => |info| try createChildProcessFailedReport(allocator, info), + .child_process_signaled => |info| try createChildProcessSignaledReport(allocator, info), + .child_process_wait_failed => |info| try createChildProcessWaitFailedReport(allocator, info), + .shared_memory_failed => |info| try createSharedMemoryFailedReport(allocator, info), + .expected_app_header => |info| try createExpectedAppHeaderReport(allocator, info), + .expected_platform_string => |info| try createExpectedPlatformStringReport(allocator, info), + .module_init_failed => |info| try createModuleInitFailedReport(allocator, info), + .no_exports_found => |info| try createNoExportsFoundReport(allocator, info), + }; + } +}; + +/// Context for file operations - helps generate better error messages +pub const FileContext = enum { + source_file, + platform_file, + module_file, + output_file, + cache_file, + target_file, + + pub fn description(self: FileContext) []const u8 { + return switch (self) { + .source_file => "source file", + .platform_file => "platform file", + .module_file => "module file", + .output_file => "output file", + .cache_file => "cache file", + .target_file => "target file", + }; + } +}; + +// Report Generation Functions + +fn createFileNotFoundReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "FILE NOT FOUND", .fatal); + + try report.document.addText("I could not find the "); + try report.document.addText(info.context.description()); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText(" "); + try report.document.addAnnotated(info.path, .path); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Please check that the path is correct and the file exists."); + + return report; +} + +fn createFileReadFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "FILE READ FAILED", .runtime_error); + + try report.document.addText("I could not read the file "); + try report.document.addAnnotated(info.path, .path); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Error: "); + try report.document.addText(@errorName(info.err)); + + return report; +} + +fn createFileWriteFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "FILE WRITE FAILED", .runtime_error); + + try report.document.addText("I could not write to the file "); + try report.document.addAnnotated(info.path, .path); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Error: "); + try report.document.addText(@errorName(info.err)); + + return report; +} + +fn createDirectoryCreateFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "DIRECTORY CREATE FAILED", .runtime_error); + + try report.document.addText("I could not create the directory "); + try report.document.addAnnotated(info.path, .path); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Error: "); + try report.document.addText(@errorName(info.err)); + + return report; +} + +fn createDirectoryNotFoundReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "DIRECTORY NOT FOUND", .runtime_error); + + try report.document.addText("The directory does not exist: "); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText(" "); + try report.document.addAnnotated(info.path, .path); + + return report; +} + +fn createTempDirFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "TEMPORARY DIRECTORY FAILED", .runtime_error); + + try report.document.addText("I could not create a temporary directory."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Error: "); + try report.document.addText(@errorName(info.err)); + + return report; +} + +fn createCacheDirUnavailableReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "CACHE DIRECTORY UNAVAILABLE", .runtime_error); + + try report.document.addText("The cache directory is not available."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Reason: "); + try report.document.addText(info.reason); + + return report; +} + +fn createNoPlatformFoundReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "NO PLATFORM FOUND", .fatal); + + try report.document.addText("The app file "); + try report.document.addAnnotated(info.app_path, .path); + try report.document.addText(" does not specify a platform."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Add a platform to your app header, for example:"); + try report.document.addLineBreak(); + try report.document.addCodeBlock( + \\app [main] { pf: platform "https://..." } + ); + + return report; +} + +fn createPlatformNotFoundReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "PLATFORM NOT FOUND", .fatal); + + try report.document.addText("I could not find the platform file:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText(" "); + try report.document.addAnnotated(info.platform_path, .path); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Please check that the platform path is correct and the file exists."); + + return report; +} + +fn createPlatformSourceNotFoundReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "PLATFORM SOURCE NOT FOUND", .runtime_error); + + try report.document.addText("Could not find the platform source file."); + try report.document.addLineBreak(); + try report.document.addText("Platform path: "); + try report.document.addAnnotated(info.platform_path, .path); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Searched in:"); + for (info.searched_paths) |path| { + try report.document.addLineBreak(); + try report.document.addText(" "); + try report.document.addAnnotated(path, .path); + } + + return report; +} + +fn createMissingPlatformModuleReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "MISSING PLATFORM MODULE", .runtime_error); + + try report.document.addText("The platform at "); + try report.document.addAnnotated(info.platform_path, .path); + try report.document.addLineBreak(); + try report.document.addText("is missing the required module: "); + try report.document.addAnnotated(info.module_name, .emphasized); + + return report; +} + +fn createMissingTypeInModuleReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "MISSING TYPE IN MODULE", .runtime_error); + + try report.document.addText("Module "); + try report.document.addAnnotated(info.module_name, .emphasized); + try report.document.addText(" does not expose a type named "); + try report.document.addAnnotated(info.type_name, .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Platform modules must expose a type with the same name as the module."); + + return report; +} + +fn createCircularPlatformDependencyReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "CIRCULAR PLATFORM DEPENDENCY", .runtime_error); + + try report.document.addText("A circular dependency was detected in the platform modules:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + for (info.module_chain) |mod| { + try report.document.addText(" "); + try report.document.addAnnotated(mod, .emphasized); + try report.document.addText(" -> "); + } + try report.document.addText("(cycle)"); + + return report; +} + +fn createPlatformValidationFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "PLATFORM VALIDATION FAILED", .runtime_error); + + try report.document.addText(info.message); + + return report; +} + +fn createAbsolutePlatformPathReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "ABSOLUTE PLATFORM PATH", .runtime_error); + + try report.document.addText("Absolute paths are not allowed for platform specifications:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText(" "); + try report.document.addAnnotated(info.platform_spec, .path); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Tip: Use a relative path like "); + try report.document.addAnnotated("../path/to/platform", .emphasized); + try report.document.addText(" or a URL."); + + return report; +} + +fn createCompilationFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "COMPILATION FAILED", .fatal); + + try report.document.addText("Compilation of "); + try report.document.addAnnotated(info.path, .path); + try report.document.addText(" failed."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + var buf: [32]u8 = undefined; + const count_str = std.fmt.bufPrint(&buf, "{}", .{info.error_count}) catch "?"; + try report.document.addText("Found "); + try report.document.addAnnotated(count_str, .error_highlight); + try report.document.addText(" error(s). See above for details."); + + return report; +} + +fn createLinkerFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "LINKER FAILED", .fatal); + + try report.document.addText("The linker failed while building for target "); + try report.document.addAnnotated(info.target, .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Error: "); + try report.document.addText(@errorName(info.err)); + + return report; +} + +fn createObjectCompilationFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "OBJECT COMPILATION FAILED", .runtime_error); + + try report.document.addText("Failed to compile object file for "); + try report.document.addAnnotated(info.path, .path); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Error: "); + try report.document.addText(@errorName(info.err)); + + return report; +} + +fn createShimGenerationFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "SHIM GENERATION FAILED", .runtime_error); + + try report.document.addText("Failed to generate the platform shim."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Error: "); + try report.document.addText(@errorName(info.err)); + + return report; +} + +fn createEntrypointExtractionFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "ENTRYPOINT EXTRACTION FAILED", .runtime_error); + + try report.document.addText("Failed to extract entrypoint from "); + try report.document.addAnnotated(info.path, .path); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Reason: "); + try report.document.addText(info.reason); + + return report; +} + +fn createInvalidUrlReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "INVALID URL", .runtime_error); + + try report.document.addText("The URL is invalid: "); + try report.document.addAnnotated(info.url, .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Reason: "); + try report.document.addText(info.reason); + + return report; +} + +fn createDownloadFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "DOWNLOAD FAILED", .runtime_error); + + try report.document.addText("Failed to download from "); + try report.document.addAnnotated(info.url, .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Error: "); + try report.document.addText(@errorName(info.err)); + + return report; +} + +fn createPackageCacheErrorReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "PACKAGE CACHE ERROR", .runtime_error); + + try report.document.addText("Error with cached package "); + try report.document.addAnnotated(info.package, .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Reason: "); + try report.document.addText(info.reason); + + return report; +} + +fn createChildProcessSpawnFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "PROCESS SPAWN FAILED", .runtime_error); + + try report.document.addText("Failed to start process: "); + try report.document.addAnnotated(info.command, .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Error: "); + try report.document.addText(@errorName(info.err)); + + return report; +} + +fn createChildProcessFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "PROCESS FAILED", .runtime_error); + + try report.document.addText("Process "); + try report.document.addAnnotated(info.command, .emphasized); + try report.document.addText(" exited with code "); + + var buf: [16]u8 = undefined; + const code_str = std.fmt.bufPrint(&buf, "{}", .{info.exit_code}) catch "?"; + try report.document.addAnnotated(code_str, .error_highlight); + + return report; +} + +fn createChildProcessSignaledReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "PROCESS SIGNALED", .runtime_error); + + try report.document.addText("Process "); + try report.document.addAnnotated(info.command, .emphasized); + try report.document.addText(" was terminated by signal "); + + var buf: [16]u8 = undefined; + const sig_str = std.fmt.bufPrint(&buf, "{}", .{info.signal}) catch "?"; + try report.document.addAnnotated(sig_str, .error_highlight); + + return report; +} + +fn createChildProcessWaitFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "PROCESS WAIT FAILED", .runtime_error); + + try report.document.addText("Failed to wait for process "); + try report.document.addAnnotated(info.command, .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Error: "); + try report.document.addText(@errorName(info.err)); + + return report; +} + +fn createSharedMemoryFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "SHARED MEMORY FAILED", .runtime_error); + + try report.document.addText("Shared memory operation '"); + try report.document.addText(info.operation); + try report.document.addText("' failed."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Error: "); + try report.document.addText(@errorName(info.err)); + + return report; +} + +fn createExpectedAppHeaderReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "EXPECTED APP HEADER", .runtime_error); + + try report.document.addText("Expected an app header in "); + try report.document.addAnnotated(info.path, .path); + try report.document.addLineBreak(); + try report.document.addText("but found: "); + try report.document.addAnnotated(info.found, .emphasized); + + return report; +} + +fn createExpectedPlatformStringReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "EXPECTED PLATFORM STRING", .runtime_error); + + try report.document.addText("Expected a platform string in the app header of "); + try report.document.addAnnotated(info.path, .path); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Example:"); + try report.document.addLineBreak(); + try report.document.addCodeBlock( + \\app [main] { pf: platform "path/to/platform" } + ); + + return report; +} + +fn createModuleInitFailedReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "MODULE INITIALIZATION FAILED", .runtime_error); + + try report.document.addText("Failed to initialize module "); + try report.document.addAnnotated(info.path, .path); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Error: "); + try report.document.addText(@errorName(info.err)); + + return report; +} + +fn createNoExportsFoundReport(allocator: Allocator, info: anytype) !Report { + var report = Report.init(allocator, "NO EXPORTS FOUND", .runtime_error); + + try report.document.addText("No exports were found in "); + try report.document.addAnnotated(info.path, .path); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + try report.document.addText("Ensure the module exports at least one definition."); + + return report; +} + +// Tests + +test "file_not_found generates correct report" { + const allocator = std.testing.allocator; + + const problem = CliProblem{ .file_not_found = .{ + .path = "app.roc", + .context = .source_file, + } }; + + var report = try problem.toReport(allocator); + defer report.deinit(); + + try std.testing.expectEqualStrings("FILE NOT FOUND", report.title); + try std.testing.expectEqual(Severity.fatal, report.severity); +} + +test "compilation_failed generates correct report" { + const allocator = std.testing.allocator; + + const problem = CliProblem{ .compilation_failed = .{ + .path = "app.roc", + .error_count = 3, + } }; + + var report = try problem.toReport(allocator); + defer report.deinit(); + + try std.testing.expectEqualStrings("COMPILATION FAILED", report.title); + try std.testing.expectEqual(Severity.fatal, report.severity); +} + +test "no_platform_found generates correct report" { + const allocator = std.testing.allocator; + + const problem = CliProblem{ .no_platform_found = .{ + .app_path = "app.roc", + } }; + + var report = try problem.toReport(allocator); + defer report.deinit(); + + try std.testing.expectEqualStrings("NO PLATFORM FOUND", report.title); + try std.testing.expectEqual(Severity.fatal, report.severity); +} + +test "severity returns correct values" { + try std.testing.expectEqual( + Severity.fatal, + (CliProblem{ .file_not_found = .{ .path = "" } }).severity(), + ); + try std.testing.expectEqual( + Severity.runtime_error, + (CliProblem{ .file_read_failed = .{ .path = "", .err = error.OutOfMemory } }).severity(), + ); +} diff --git a/src/cli/REORGANIZATION.md b/src/cli/REORGANIZATION.md new file mode 100644 index 0000000000..760982ecc9 --- /dev/null +++ b/src/cli/REORGANIZATION.md @@ -0,0 +1,90 @@ +# CLI Directory Reorganization Plan + +This document outlines future work to reorganize the CLI module for better maintainability and idiomatic Zig conventions. + +## Design Principles + +1. **Flat structure** - No subdirectories, use descriptive filenames to group related code +2. **TitleCase.zig** for files defining a single primary type (e.g., `CliContext.zig`) +3. **snake_case.zig** for namespace/function modules (e.g., `cli_args.zig`) +4. **cli_roc_* prefix** for command implementation files to avoid conflicts +5. Direct imports rather than re-export hubs + +## Current State + +The CLI module is functional but `main.zig` is ~5,500 lines containing all command implementations. + +## Future Structure + +``` +src/cli/ +├── main.zig # Slim entrypoint (~300 lines): dispatch only +│ +├── CliContext.zig # Main CLI context type (DONE) +├── CliProblem.zig # Runtime error types (DONE) +├── cli_args.zig # Argument parsing (ArgProblem renamed, DONE) +│ +├── cli_roc_run.zig # rocRun command implementation +├── cli_roc_build.zig # rocBuild command implementation +├── cli_roc_check.zig # rocCheck command implementation +├── cli_roc_test.zig # rocTest command implementation +├── cli_roc_format.zig # rocFormat command implementation +├── cli_roc_docs.zig # rocDocs command implementation +├── cli_roc_bundle.zig # rocBundle command implementation +├── cli_roc_unbundle.zig # rocUnbundle command implementation +│ +├── platform_resolution.zig # Platform spec parsing, path resolution +├── platform_cache.zig # getRocCacheDir, URL platform resolution +├── platform_validation.zig # Platform header validation (existing) +│ +├── compile_serialization.zig # compileAndSerializeModulesForEmbedding +├── compile_shared_memory.zig # POSIX/Windows shared memory +│ +├── builder.zig # LLVM bitcode compilation (existing) +├── linker.zig # LLD wrapper (existing) +├── platform_host_shim.zig # LLVM shim generation (existing) +│ +├── target.zig # Target definitions (existing) +├── targets_validator.zig # Targets section validation (existing) +├── cross_compilation.zig # Cross-compilation validation (existing) +├── libc_finder.zig # Linux libc detection (existing) +│ +├── util_windows.zig # Windows console functions +├── util_posix.zig # POSIX shm wrappers +├── util_timing.zig # formatElapsedTime +│ +├── bench.zig # Benchmarking utility (existing) +├── targets/ # Pre-compiled shim libraries (keep) +└── test/ # Test files (keep) +``` + +## Migration Phases + +### Phase 1: Extract Utilities from main.zig +1. Create `util_windows.zig` - Windows console functions +2. Create `util_posix.zig` - POSIX shm wrappers +3. Create `util_timing.zig` - formatElapsedTime, printTimingBreakdown + +### Phase 2: Extract Compilation Infrastructure +1. Create `compile_shared_memory.zig` - SharedMemoryHandle, write functions +2. Create `compile_serialization.zig` - compileAndSerializeModulesForEmbedding + +### Phase 3: Extract Platform Resolution +1. Create `platform_resolution.zig` - extractPlatformSpecFromApp, resolvePlatformPaths +2. Create `platform_cache.zig` - getRocCacheDir, resolveUrlPlatform + +### Phase 4: Extract Commands (Ordered by Complexity) +1. `cli_roc_format.zig` (simplest) +2. `cli_roc_unbundle.zig` +3. `cli_roc_bundle.zig` +4. `cli_roc_test.zig` +5. `cli_roc_check.zig` +6. `cli_roc_docs.zig` +7. `cli_roc_build.zig` +8. `cli_roc_run.zig` (most complex) + +### Phase 5: Slim main.zig +Reduce main.zig to ~300 lines containing only: +- Entry point +- Command dispatch +- Top-level error handling diff --git a/src/cli/bench.zig b/src/cli/bench.zig index 62f6841c6f..0119fece00 100644 --- a/src/cli/bench.zig +++ b/src/cli/bench.zig @@ -8,6 +8,7 @@ const can = @import("can"); const tracy = @import("tracy"); const fmt = @import("fmt"); +const builtin = @import("builtin"); const tokenize = parse.tokenize; const ModuleEnv = can.ModuleEnv; @@ -15,6 +16,19 @@ const CommonEnv = base.CommonEnv; const Allocator = std.mem.Allocator; +const is_windows = builtin.target.os.tag == .windows; + +var stderr_file_writer: std.fs.File.Writer = .{ + .interface = std.fs.File.Writer.initInterface(&.{}), + .file = if (is_windows) undefined else std.fs.File.stderr(), + .mode = .streaming, +}; + +fn stderrWriter() *std.Io.Writer { + if (is_windows) stderr_file_writer.file = std.fs.File.stderr(); + return &stderr_file_writer.interface; +} + const RocFile = struct { path: []const u8, content: []const u8, @@ -40,7 +54,7 @@ fn benchParseOrTokenize(comptime is_parse: bool, gpa: Allocator, path: []const u std.debug.print("Benchmarking {s} on '{s}'\n", .{ operation_name, path }); // Find all .roc files (from file or directory) - var roc_files = std.ArrayList(RocFile).init(gpa); + var roc_files = std.array_list.Managed(RocFile).init(gpa); defer { for (roc_files.items) |roc_file| { gpa.free(roc_file.path); @@ -64,6 +78,9 @@ fn benchParseOrTokenize(comptime is_parse: bool, gpa: Allocator, path: []const u std.debug.print("Total: {} bytes, {} lines\n", .{ metrics.total_bytes, metrics.total_lines }); // Create a module environment for tokenization (reused for tokenizer, created per-iteration for parser) + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + var env: ?ModuleEnv = if (!is_parse) try ModuleEnv.init(gpa, "") else null; defer if (env) |*e| e.deinit(); @@ -88,12 +105,15 @@ fn benchParseOrTokenize(comptime is_parse: bool, gpa: Allocator, path: []const u // ModuleEnv takes ownership of the source code, so we need to dupe it each iteration const source_copy = try gpa.dupe(u8, roc_file.content); + var parse_arena = std.heap.ArenaAllocator.init(gpa); + var parse_env = try ModuleEnv.init(gpa, source_copy); var ir = try parse.parse(&parse_env.common, gpa); iteration_tokens += ir.tokens.tokens.len; ir.deinit(gpa); parse_env.deinit(); + parse_arena.deinit(); } else { // Tokenize mode var messages: [128]tokenize.Diagnostic = undefined; @@ -101,7 +121,7 @@ fn benchParseOrTokenize(comptime is_parse: bool, gpa: Allocator, path: []const u var tokenizer = try tokenize.Tokenizer.init(&env.?.common, gpa, roc_file.content, msg_slice); try tokenizer.tokenize(gpa); - var result = tokenizer.finishAndDeinit(gpa); + var result = tokenizer.finishAndDeinit(); iteration_tokens += result.tokens.tokens.len; result.tokens.deinit(gpa); } @@ -140,7 +160,7 @@ pub fn benchTokenizer(gpa: Allocator, path: []const u8) !void { try benchParseOrTokenize(false, gpa, path); } -fn collectRocFiles(gpa: Allocator, path: []const u8, roc_files: *std.ArrayList(RocFile)) !void { +fn collectRocFiles(gpa: Allocator, path: []const u8, roc_files: *std.array_list.Managed(RocFile)) !void { // Check if path is a file or directory const stat = std.fs.cwd().statFile(path) catch |err| { fatal("Failed to access '{s}': {}", .{ path, err }); @@ -163,7 +183,7 @@ fn collectRocFiles(gpa: Allocator, path: []const u8, roc_files: *std.ArrayList(R } } -fn addRocFile(gpa: Allocator, file_path: []const u8, roc_files: *std.ArrayList(RocFile)) !void { +fn addRocFile(gpa: Allocator, file_path: []const u8, roc_files: *std.array_list.Managed(RocFile)) !void { const file = std.fs.cwd().openFile(file_path, .{}) catch |err| { std.debug.print("Warning: Failed to open file '{s}': {}\n", .{ file_path, err }); return; @@ -185,7 +205,7 @@ fn addRocFile(gpa: Allocator, file_path: []const u8, roc_files: *std.ArrayList(R }); } -fn findRocFiles(gpa: Allocator, dir_path: []const u8, roc_files: *std.ArrayList(RocFile)) !void { +fn findRocFiles(gpa: Allocator, dir_path: []const u8, roc_files: *std.array_list.Managed(RocFile)) !void { var dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch |err| { fatal("Failed to open directory '{s}': {}", .{ dir_path, err }); }; @@ -281,7 +301,7 @@ fn printBenchmarkResults(benchmark_name: []const u8, results: BenchmarkResults) /// Log a fatal error and exit the process with a non-zero code. pub fn fatal(comptime format: []const u8, args: anytype) noreturn { - std.io.getStdErr().writer().print(format, args) catch unreachable; + stderrWriter().print(format, args) catch unreachable; if (tracy.enable) { tracy.waitForShutdown() catch unreachable; } diff --git a/src/cli/builder.zig b/src/cli/builder.zig new file mode 100644 index 0000000000..574b63d8b4 --- /dev/null +++ b/src/cli/builder.zig @@ -0,0 +1,484 @@ +//! LLVM-based compilation infrastructure for Roc + +const std = @import("std"); +const builtin = @import("builtin"); +const target = @import("target.zig"); +const reporting = @import("reporting"); + +const Allocator = std.mem.Allocator; + +const is_windows = builtin.target.os.tag == .windows; + +var stderr_file_writer: std.fs.File.Writer = .{ + .interface = std.fs.File.Writer.initInterface(&.{}), + .file = if (is_windows) undefined else std.fs.File.stderr(), + .mode = .streaming, +}; + +fn stderrWriter() *std.Io.Writer { + if (is_windows) stderr_file_writer.file = std.fs.File.stderr(); + return &stderr_file_writer.interface; +} + +// Re-export RocTarget from target.zig for backward compatibility +pub const RocTarget = target.RocTarget; + +/// Optimization levels for compilation +pub const OptimizationLevel = enum { + none, // --opt none (no optimizations) + size, // --opt size (optimize for binary size) + speed, // --opt speed (aggressive performance optimizations) + + /// Convert to LLVM optimization level + fn toLLVMLevel(self: OptimizationLevel) c_int { + return switch (self) { + .none => LLVMCodeGenLevelNone, + .size => LLVMCodeGenLevelLess, + .speed => LLVMCodeGenLevelAggressive, + }; + } +}; + +/// Configuration for compiling LLVM bitcode to object files +pub const CompileConfig = struct { + input_path: []const u8, + output_path: []const u8, + optimization: OptimizationLevel, + target: RocTarget, + cpu: []const u8 = "", + features: []const u8 = "", + debug: bool = false, // Enable debug info generation in output + + /// Check if compiling for the current machine + pub fn isNative(self: CompileConfig) bool { + return self.target == RocTarget.detectNative(); + } +}; + +// Check if LLVM is available at compile time +const llvm_available = if (@import("builtin").is_test) false else @import("config").llvm; + +// LLVM ABI Types +const ZigLLVMABIType = enum(c_int) { + ZigLLVMABITypeDefault = 0, + ZigLLVMABITypeSoft, + ZigLLVMABITypeHard, +}; + +const ZigLLVMCoverageType = enum(c_int) { + ZigLLVMCoverageType_None = 0, + ZigLLVMCoverageType_Function, + ZigLLVMCoverageType_BB, + ZigLLVMCoverageType_Edge, +}; + +const ZigLLVMCoverageOptions = extern struct { + CoverageType: ZigLLVMCoverageType, + IndirectCalls: bool, + TraceBB: bool, + TraceCmp: bool, + TraceDiv: bool, + TraceGep: bool, + Use8bitCounters: bool, + TracePC: bool, + TracePCGuard: bool, + Inline8bitCounters: bool, + InlineBoolFlag: bool, + PCTable: bool, + NoPrune: bool, + StackDepth: bool, + TraceLoads: bool, + TraceStores: bool, + CollectControlFlow: bool, +}; + +const ZigLLVMThinOrFullLTOPhase = enum(c_int) { + ZigLLVMThinOrFullLTOPhase_None, + ZigLLVMThinOrFullLTOPhase_ThinPreLink, + ZigLLVMThinOrFullLTOPhase_ThinkPostLink, + ZigLLVMThinOrFullLTOPhase_FullPreLink, + ZigLLVMThinOrFullLTOPhase_FullPostLink, +}; + +const ZigLLVMEmitOptions = extern struct { + is_debug: bool, + is_small: bool, + time_report_out: ?*?[*:0]u8, + tsan: bool, + sancov: bool, + lto: ZigLLVMThinOrFullLTOPhase, + allow_fast_isel: bool, + allow_machine_outliner: bool, + asm_filename: ?[*:0]const u8, + bin_filename: ?[*:0]const u8, + llvm_ir_filename: ?[*:0]const u8, + bitcode_filename: ?[*:0]const u8, + coverage: ZigLLVMCoverageOptions, +}; + +// LLVM Code Generation Optimization Levels +const LLVMCodeGenLevelNone: c_int = 0; +const LLVMCodeGenLevelLess: c_int = 1; +const LLVMCodeGenLevelDefault: c_int = 2; +const LLVMCodeGenLevelAggressive: c_int = 3; + +// LLVM Relocation Models +const LLVMRelocDefault: c_int = 0; +const LLVMRelocStatic: c_int = 1; +const LLVMRelocPIC: c_int = 2; +const LLVMRelocDynamicNoPic: c_int = 3; +const LLVMRelocROPI: c_int = 4; +const LLVMRelocRWPI: c_int = 5; +const LLVMRelocROPI_RWPI: c_int = 6; + +// LLVM Code Models +const LLVMCodeModelDefault: c_int = 0; +const LLVMCodeModelJITDefault: c_int = 1; +const LLVMCodeModelTiny: c_int = 2; +const LLVMCodeModelSmall: c_int = 3; +const LLVMCodeModelKernel: c_int = 4; +const LLVMCodeModelMedium: c_int = 5; +const LLVMCodeModelLarge: c_int = 6; + +// External C functions from zig_llvm.cpp and LLVM C API - only available when LLVM is enabled +const llvm_externs = if (llvm_available) struct { + extern fn ZigLLVMTargetMachineEmitToFile( + targ_machine_ref: ?*anyopaque, + module_ref: ?*anyopaque, + error_message: *[*:0]u8, + options: *const ZigLLVMEmitOptions, + ) bool; + extern fn ZigLLVMCreateTargetMachine( + target_ref: ?*anyopaque, + triple: [*:0]const u8, + cpu: [*:0]const u8, + features: [*:0]const u8, + level: c_int, // LLVMCodeGenOptLevel + reloc: c_int, // LLVMRelocMode + code_model: c_int, // LLVMCodeModel + function_sections: bool, + data_sections: bool, + float_abi: ZigLLVMABIType, + abi_name: ?[*:0]const u8, + emulated_tls: bool, + ) ?*anyopaque; + + // LLVM wrapper functions + extern fn ZigLLVMInitializeAllTargets() void; + + // LLVM C API functions + extern fn LLVMGetDefaultTargetTriple() [*:0]u8; + extern fn LLVMGetTargetFromTriple(triple: [*:0]const u8, target: *?*anyopaque, error_message: *[*:0]u8) c_int; + extern fn LLVMDisposeMessage(message: [*:0]u8) void; + extern fn LLVMCreateMemoryBufferWithContentsOfFile(path: [*:0]const u8, out_mem_buf: *?*anyopaque, out_message: *[*:0]u8) c_int; + extern fn LLVMParseBitcode(mem_buf: ?*anyopaque, out_module: *?*anyopaque, out_message: *[*:0]u8) c_int; + extern fn LLVMDisposeMemoryBuffer(mem_buf: ?*anyopaque) void; + extern fn LLVMDisposeModule(module: ?*anyopaque) void; + extern fn LLVMDisposeTargetMachine(target_machine: ?*anyopaque) void; + extern fn LLVMSetTarget(module: ?*anyopaque, triple: [*:0]const u8) void; +} else struct {}; + +/// Initialize LLVM targets (must be called once before using LLVM) +pub fn initializeLLVM() void { + if (comptime !llvm_available) { + return; + } + const externs = llvm_externs; + externs.ZigLLVMInitializeAllTargets(); +} + +/// Compile LLVM bitcode file to object file +pub fn compileBitcodeToObject(gpa: Allocator, config: CompileConfig) !bool { + if (comptime !llvm_available) { + renderLLVMNotAvailableError(gpa); + return error.LLVMNotAvailable; + } + + const externs = llvm_externs; + + std.log.debug("Starting bitcode to object compilation", .{}); + std.log.debug("Input: {s} -> Output: {s}", .{ config.input_path, config.output_path }); + std.log.debug("Target: {} ({s})", .{ config.target, config.target.toTriple() }); + std.log.debug("Optimization: {}", .{config.optimization}); + std.log.debug("CPU: '{s}', Features: '{s}'", .{ config.cpu, config.features }); + + // Verify input file exists + std.fs.cwd().access(config.input_path, .{}) catch |err| { + renderFileNotAccessibleError(gpa, config.input_path, err); + return false; + }; + + // 1. Initialize LLVM targets + std.log.debug("Initializing LLVM targets...", .{}); + initializeLLVM(); + std.log.debug("LLVM targets initialized successfully", .{}); + + // 2. Load bitcode file + std.log.debug("Loading bitcode file: {s}", .{config.input_path}); + var mem_buf: ?*anyopaque = null; + var error_message: [*:0]u8 = undefined; + + const bitcode_path_z = try gpa.dupeZ(u8, config.input_path); + defer gpa.free(bitcode_path_z); + + if (externs.LLVMCreateMemoryBufferWithContentsOfFile(bitcode_path_z.ptr, &mem_buf, &error_message) != 0) { + renderLLVMError(gpa, "BITCODE LOAD ERROR", "Failed to load bitcode file", std.mem.span(error_message)); + externs.LLVMDisposeMessage(error_message); + return false; + } + defer if (mem_buf) |buf| externs.LLVMDisposeMemoryBuffer(buf); + std.log.debug("Bitcode file loaded successfully", .{}); + + // 3. Parse bitcode into module + std.log.debug("Parsing bitcode into LLVM module...", .{}); + var module: ?*anyopaque = null; + if (externs.LLVMParseBitcode(mem_buf, &module, &error_message) != 0) { + renderLLVMError(gpa, "BITCODE PARSE ERROR", "Failed to parse bitcode", std.mem.span(error_message)); + externs.LLVMDisposeMessage(error_message); + return false; + } + defer if (module) |mod| externs.LLVMDisposeModule(mod); + std.log.debug("Bitcode parsed successfully", .{}); + + // 4. Get target triple and set it on the module + const target_triple = config.target.toTriple(); + const target_triple_z = try gpa.dupeZ(u8, target_triple); + defer gpa.free(target_triple_z); + + std.log.debug("Setting target triple on module: {s}", .{target_triple}); + externs.LLVMSetTarget(module, target_triple_z.ptr); + std.log.debug("Target triple set successfully", .{}); + + // 5. Create target + std.log.debug("Getting LLVM target for triple: {s}", .{target_triple}); + var llvm_target: ?*anyopaque = null; + if (externs.LLVMGetTargetFromTriple(target_triple_z.ptr, &llvm_target, &error_message) != 0) { + renderTargetError(gpa, target_triple, std.mem.span(error_message)); + externs.LLVMDisposeMessage(error_message); + return false; + } + std.log.debug("LLVM target obtained successfully", .{}); + + // 6. Create target machine + const cpu_z = try gpa.dupeZ(u8, config.cpu); + defer gpa.free(cpu_z); + const features_z = try gpa.dupeZ(u8, config.features); + defer gpa.free(features_z); + + std.log.debug("Creating target machine with CPU='{s}', Features='{s}'", .{ config.cpu, config.features }); + const target_machine = externs.ZigLLVMCreateTargetMachine( + llvm_target, + target_triple_z.ptr, + cpu_z.ptr, + features_z.ptr, + config.optimization.toLLVMLevel(), + LLVMRelocDefault, + LLVMCodeModelDefault, + false, // function_sections + false, // data_sections + .ZigLLVMABITypeDefault, // float_abi + null, // abi_name + false, // emulated_tls + ); + if (target_machine == null) { + renderTargetMachineError(gpa, target_triple, config.cpu, config.features); + return false; + } + defer externs.LLVMDisposeTargetMachine(target_machine); + std.log.debug("Target machine created successfully", .{}); + + // 7. Prepare output path + const object_path_z = try gpa.dupeZ(u8, config.output_path); + defer gpa.free(object_path_z); + + // 8. Emit object file + std.log.debug("Emitting object file to: {s}", .{config.output_path}); + var emit_error_message: [*:0]u8 = undefined; + + var coverage_options = std.mem.zeroes(ZigLLVMCoverageOptions); + coverage_options.CoverageType = .ZigLLVMCoverageType_None; + + const emit_options = ZigLLVMEmitOptions{ + // Auto-enable debug when roc is built in debug mode, OR when explicitly requested via --debug + .is_debug = (builtin.mode == .Debug) or config.debug, + .is_small = config.optimization == .size, + .time_report_out = null, + .tsan = false, + .sancov = false, + .lto = .ZigLLVMThinOrFullLTOPhase_None, + .allow_fast_isel = false, + .allow_machine_outliner = true, + .asm_filename = null, + .bin_filename = object_path_z.ptr, + .llvm_ir_filename = null, + .bitcode_filename = null, + .coverage = coverage_options, + }; + + const emit_result = externs.ZigLLVMTargetMachineEmitToFile( + target_machine, + module, + &emit_error_message, + &emit_options, + ); + + if (emit_result) { + renderEmitError(gpa, config.output_path, std.mem.span(emit_error_message)); + externs.LLVMDisposeMessage(emit_error_message); + return false; + } + + std.log.debug("Successfully compiled bitcode to object file: {s}", .{config.output_path}); + return true; +} + +/// Check if LLVM is available +pub fn isLLVMAvailable() bool { + return llvm_available; +} + +// --- Error Reporting Helpers --- + +fn renderLLVMNotAvailableError(allocator: Allocator) void { + var report = reporting.Report.init(allocator, "LLVM NOT AVAILABLE", .fatal); + defer report.deinit(); + + report.document.addText("LLVM is not available at compile time.") catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText("This binary was built without LLVM support.") catch return; + report.document.addLineBreak() catch return; + report.document.addText("To use this feature, rebuild roc with LLVM enabled.") catch return; + report.document.addLineBreak() catch return; + + reporting.renderReportToTerminal( + &report, + stderrWriter(), + .ANSI, + reporting.ReportingConfig.initColorTerminal(), + ) catch {}; +} + +fn renderFileNotAccessibleError(allocator: Allocator, path: []const u8, err: anyerror) void { + var report = reporting.Report.init(allocator, "FILE NOT ACCESSIBLE", .fatal); + defer report.deinit(); + + report.document.addText("Input bitcode file does not exist or is not accessible:") catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText(" ") catch return; + report.document.addAnnotated(path, .path) catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText("Error: ") catch return; + report.document.addAnnotated(@errorName(err), .error_highlight) catch return; + report.document.addLineBreak() catch return; + + reporting.renderReportToTerminal( + &report, + stderrWriter(), + .ANSI, + reporting.ReportingConfig.initColorTerminal(), + ) catch {}; +} + +fn renderLLVMError(allocator: Allocator, title: []const u8, message: []const u8, llvm_message: []const u8) void { + var report = reporting.Report.init(allocator, title, .fatal); + defer report.deinit(); + + report.document.addText(message) catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText("LLVM error: ") catch return; + report.document.addAnnotated(llvm_message, .error_highlight) catch return; + report.document.addLineBreak() catch return; + + reporting.renderReportToTerminal( + &report, + stderrWriter(), + .ANSI, + reporting.ReportingConfig.initColorTerminal(), + ) catch {}; +} + +fn renderTargetError(allocator: Allocator, triple: []const u8, llvm_message: []const u8) void { + var report = reporting.Report.init(allocator, "INVALID TARGET", .fatal); + defer report.deinit(); + + report.document.addText("Failed to get LLVM target for triple:") catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText(" ") catch return; + report.document.addAnnotated(triple, .emphasized) catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText("LLVM error: ") catch return; + report.document.addAnnotated(llvm_message, .error_highlight) catch return; + report.document.addLineBreak() catch return; + + reporting.renderReportToTerminal( + &report, + stderrWriter(), + .ANSI, + reporting.ReportingConfig.initColorTerminal(), + ) catch {}; +} + +fn renderTargetMachineError(allocator: Allocator, triple: []const u8, cpu: []const u8, features: []const u8) void { + var report = reporting.Report.init(allocator, "TARGET MACHINE ERROR", .fatal); + defer report.deinit(); + + report.document.addText("Failed to create LLVM target machine with configuration:") catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText(" Triple: ") catch return; + report.document.addAnnotated(triple, .emphasized) catch return; + report.document.addLineBreak() catch return; + report.document.addText(" CPU: ") catch return; + if (cpu.len > 0) { + report.document.addAnnotated(cpu, .emphasized) catch return; + } else { + report.document.addText("(default)") catch return; + } + report.document.addLineBreak() catch return; + report.document.addText(" Features: ") catch return; + if (features.len > 0) { + report.document.addAnnotated(features, .emphasized) catch return; + } else { + report.document.addText("(default)") catch return; + } + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText("This may indicate an unsupported target configuration.") catch return; + report.document.addLineBreak() catch return; + + reporting.renderReportToTerminal( + &report, + stderrWriter(), + .ANSI, + reporting.ReportingConfig.initColorTerminal(), + ) catch {}; +} + +fn renderEmitError(allocator: Allocator, output_path: []const u8, llvm_message: []const u8) void { + var report = reporting.Report.init(allocator, "OBJECT FILE EMIT ERROR", .fatal); + defer report.deinit(); + + report.document.addText("Failed to emit object file:") catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText(" Output: ") catch return; + report.document.addAnnotated(output_path, .path) catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText("LLVM error: ") catch return; + report.document.addAnnotated(llvm_message, .error_highlight) catch return; + report.document.addLineBreak() catch return; + + reporting.renderReportToTerminal( + &report, + stderrWriter(), + .ANSI, + reporting.ReportingConfig.initColorTerminal(), + ) catch {}; +} diff --git a/src/cli/cli_args.zig b/src/cli/cli_args.zig index 72b43918fe..5cd959743e 100644 --- a/src/cli/cli_args.zig +++ b/src/cli/cli_args.zig @@ -11,30 +11,31 @@ pub const CliArgs = union(enum) { run: RunArgs, check: CheckArgs, build: BuildArgs, - format: FormatArgs, + fmt: FormatArgs, test_cmd: TestArgs, bundle: BundleArgs, unbundle: UnbundleArgs, repl, version, docs: DocsArgs, + experimental_lsp: ExperimentalLspArgs, help: []const u8, licenses, - problem: CliProblem, + problem: ArgProblem, - pub fn deinit(self: CliArgs, gpa: mem.Allocator) void { + pub fn deinit(self: CliArgs, alloc: mem.Allocator) void { switch (self) { - .format => |fmt| gpa.free(fmt.paths), - .run => |run| gpa.free(run.app_args), - .bundle => |bundle| gpa.free(bundle.paths), - .unbundle => |unbundle| gpa.free(unbundle.paths), + .fmt => |fmt| alloc.free(fmt.paths), + .run => |run| alloc.free(run.app_args), + .bundle => |bundle| alloc.free(bundle.paths), + .unbundle => |unbundle| alloc.free(unbundle.paths), else => return, } } }; /// Errors that can occur due to bad input while parsing the arguments -pub const CliProblem = union(enum) { +pub const ArgProblem = union(enum) { missing_flag_value: struct { flag: []const u8, }, @@ -64,8 +65,10 @@ pub const OptLevel = enum { pub const RunArgs = struct { path: []const u8, // the path of the roc file to be executed opt: OptLevel = .dev, // the optimization level + target: ?[]const u8 = null, // the target to compile for (e.g., x64musl, x64glibc) app_args: []const []const u8 = &[_][]const u8{}, // any arguments to be passed to roc application being run no_cache: bool = false, // bypass the executable cache + allow_errors: bool = false, // allow execution even if there are type errors }; /// Arguments for `roc check` @@ -81,9 +84,15 @@ pub const CheckArgs = struct { pub const BuildArgs = struct { path: []const u8, // the path to the roc file to be built opt: OptLevel, // the optimization level + target: ?[]const u8 = null, // the target to compile for (e.g., x64musl, x64glibc) output: ?[]const u8 = null, // the path where the output binary should be created + debug: bool = false, // include debug information in the output binary + allow_errors: bool = false, // allow building even if there are type errors + wasm_memory: ?usize = null, // initial memory size for WASM targets (default: 64MB) + wasm_stack_size: ?usize = null, // stack size for WASM targets (default: 8MB) z_bench_tokenize: ?[]const u8 = null, // benchmark tokenizer on a file or directory z_bench_parse: ?[]const u8 = null, // benchmark parser on a file or directory + z_dump_linker: bool = false, // dump linker inputs to temp directory for debugging }; /// Arguments for `roc test` @@ -115,28 +124,46 @@ pub const UnbundleArgs = struct { /// Arguments for `roc docs` pub const DocsArgs = struct { - path: []const u8, // the main.roc file to base the generation on - output: []const u8, // the path to the output directory for the generated docs - root_dir: ?[]const u8 = null, // the prefix to be used in generated links in the docs + path: []const u8, // the path of the roc file to generate docs for + main: ?[]const u8 = null, // the path to a roc file with an app header to be used to resolved dependencies + output: []const u8 = "generated-docs", // the output directory for generated documentation + time: bool = false, // whether to print timing information + no_cache: bool = false, // disable cache + verbose: bool = false, // enable verbose output + serve: bool = false, // start an HTTP server after generating docs +}; + +/// Arguments for `roc experimental-lsp` +pub const ExperimentalLspArgs = struct { + debug_io: bool = false, // log the LSP messages to a temporary log file + debug_build: bool = false, + debug_syntax: bool = false, + debug_server: bool = false, }; /// Parse a list of arguments. -pub fn parse(gpa: mem.Allocator, args: []const []const u8) !CliArgs { - if (args.len == 0) return try parseRun(gpa, args); +pub fn parse(alloc: mem.Allocator, args: []const []const u8) !CliArgs { + if (args.len == 0) return try parseRun(alloc, args); + // "run" is not a valid subcommand - give a helpful error + // The correct usage is: roc path/to/app.roc (without "run") + if (mem.eql(u8, args[0], "run")) { + return CliArgs{ .help = run_not_a_command_help }; + } if (mem.eql(u8, args[0], "check")) return parseCheck(args[1..]); if (mem.eql(u8, args[0], "build")) return parseBuild(args[1..]); - if (mem.eql(u8, args[0], "bundle")) return try parseBundle(gpa, args[1..]); - if (mem.eql(u8, args[0], "unbundle")) return try parseUnbundle(gpa, args[1..]); - if (mem.eql(u8, args[0], "format")) return try parseFormat(gpa, args[1..]); + if (mem.eql(u8, args[0], "bundle")) return try parseBundle(alloc, args[1..]); + if (mem.eql(u8, args[0], "unbundle")) return try parseUnbundle(alloc, args[1..]); + if (mem.eql(u8, args[0], "fmt")) return try parseFormat(alloc, args[1..]); if (mem.eql(u8, args[0], "test")) return parseTest(args[1..]); if (mem.eql(u8, args[0], "repl")) return parseRepl(args[1..]); if (mem.eql(u8, args[0], "version")) return parseVersion(args[1..]); if (mem.eql(u8, args[0], "docs")) return parseDocs(args[1..]); + if (mem.eql(u8, args[0], "experimental-lsp")) return parseExperimentalLsp(args[1..]); if (mem.eql(u8, args[0], "help")) return CliArgs{ .help = main_help }; if (mem.eql(u8, args[0], "licenses")) return parseLicenses(args[1..]); - return try parseRun(gpa, args); + return try parseRun(alloc, args); } const main_help = @@ -152,10 +179,11 @@ const main_help = \\ unbundle Extract files from compressed .tar.zst archives \\ test Run all top-level `expect`s in a main module and any modules it imports \\ repl Launch the interactive Read Eval Print Loop (REPL) - \\ format Format a .roc file or the .roc files contained in a directory using standard Roc formatting + \\ fmt Format a .roc file or the .roc files contained in a directory using standard Roc formatting \\ version Print the Roc compiler's version \\ check Check the code for problems, but don't build or run it \\ docs Generate documentation for a Roc package or platform + \\ experimental-lsp Start the experimental language server (LSP) implementation \\ help Print this message \\ licenses Prints license info for Roc as well as attributions to other projects used by Roc \\ @@ -165,6 +193,23 @@ const main_help = \\ e.g. `roc run -- arg1 arg2` \\Options: \\ --opt= Optimize the build process for binary size, execution speed, or compilation speed. Defaults to compilation speed (dev) + \\ --target= Target to compile for (e.g., x64musl, x64glibc, arm64musl). Defaults to native target with musl for static linking + \\ --no-cache Force a rebuild of the interpreted host (useful for compiler and platform developers) + \\ --allow-errors Allow execution even if there are type errors (warnings are always allowed) + \\ +; + +const run_not_a_command_help = + \\Error: 'run' is not a valid subcommand. + \\ + \\To run a Roc application, use: + \\ roc path/to/app.roc + \\ + \\For example: + \\ roc main.roc Run main.roc in the current directory + \\ roc examples/hello.roc Run hello.roc from the examples folder + \\ + \\Use 'roc help' to see all available commands. \\ ; @@ -197,7 +242,7 @@ fn parseCheck(args: []const []const u8) CliArgs { if (getFlagValue(arg)) |value| { main = value; } else { - return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--main" } } }; + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--main" } } }; } } else if (mem.eql(u8, arg, "--time")) { time = true; @@ -207,7 +252,7 @@ fn parseCheck(args: []const []const u8) CliArgs { verbose = true; } else { if (path != null) { - return CliArgs{ .problem = CliProblem{ .unexpected_argument = .{ .cmd = "check", .arg = arg } } }; + return CliArgs{ .problem = ArgProblem{ .unexpected_argument = .{ .cmd = "check", .arg = arg } } }; } path = arg; } @@ -219,9 +264,15 @@ fn parseCheck(args: []const []const u8) CliArgs { fn parseBuild(args: []const []const u8) CliArgs { var path: ?[]const u8 = null; var opt: OptLevel = .dev; + var target: ?[]const u8 = null; var output: ?[]const u8 = null; + var debug: bool = false; + var allow_errors: bool = false; + var wasm_memory: ?usize = null; + var wasm_stack_size: ?usize = null; var z_bench_tokenize: ?[]const u8 = null; var z_bench_parse: ?[]const u8 = null; + var z_dump_linker: bool = false; for (args) |arg| { if (isHelpFlag(arg)) { return CliArgs{ .help = @@ -235,51 +286,85 @@ fn parseBuild(args: []const []const u8) CliArgs { \\Options: \\ --output= The full path to the output binary, including filename. To specify directory only, specify a path that ends in a directory separator (e.g. a slash) \\ --opt= Optimize the build process for binary size, execution speed, or compilation speed. Defaults to compilation speed (dev) + \\ --target= Target to compile for (e.g., x64musl, x64glibc, arm64musl). Defaults to native target with musl for static linking + \\ --debug Include debug information in the output binary + \\ --allow-errors Allow building even if there are type errors (warnings are always allowed) + \\ --wasm-memory= Initial memory size for WASM targets in bytes (default: 67108864 = 64MB) + \\ --wasm-stack-size= Stack size for WASM targets in bytes (default: 8388608 = 8MB) \\ --z-bench-tokenize= Benchmark tokenizer on a file or directory \\ --z-bench-parse= Benchmark parser on a file or directory + \\ --z-dump-linker Dump linker inputs to temp directory for debugging \\ -h, --help Print help \\ }; + } else if (mem.startsWith(u8, arg, "--target")) { + if (getFlagValue(arg)) |value| { + target = value; + } else { + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--target" } } }; + } } else if (mem.startsWith(u8, arg, "--output")) { if (getFlagValue(arg)) |value| { output = value; } else { - return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--output" } } }; + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--output" } } }; } } else if (mem.startsWith(u8, arg, "--opt")) { if (getFlagValue(arg)) |value| { if (OptLevel.from_str(value)) |level| { opt = level; } else { - return CliArgs{ .problem = CliProblem{ .invalid_flag_value = .{ .flag = "--opt", .value = value, .valid_options = "speed,size,dev" } } }; + return CliArgs{ .problem = ArgProblem{ .invalid_flag_value = .{ .flag = "--opt", .value = value, .valid_options = "speed,size,dev" } } }; } } else { - return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--opt" } } }; + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--opt" } } }; } } else if (mem.startsWith(u8, arg, "--z-bench-tokenize")) { if (getFlagValue(arg)) |value| { z_bench_tokenize = value; } else { - return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--z-bench-tokenize" } } }; + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--z-bench-tokenize" } } }; } } else if (mem.startsWith(u8, arg, "--z-bench-parse")) { if (getFlagValue(arg)) |value| { z_bench_parse = value; } else { - return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--z-bench-parse" } } }; + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--z-bench-parse" } } }; } + } else if (mem.eql(u8, arg, "--debug")) { + debug = true; + } else if (mem.eql(u8, arg, "--allow-errors")) { + allow_errors = true; + } else if (mem.startsWith(u8, arg, "--wasm-memory")) { + if (getFlagValue(arg)) |value| { + wasm_memory = std.fmt.parseInt(usize, value, 10) catch { + return CliArgs{ .problem = ArgProblem{ .invalid_flag_value = .{ .flag = "--wasm-memory", .value = value, .valid_options = "positive integer (bytes)" } } }; + }; + } else { + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--wasm-memory" } } }; + } + } else if (mem.startsWith(u8, arg, "--wasm-stack-size")) { + if (getFlagValue(arg)) |value| { + wasm_stack_size = std.fmt.parseInt(usize, value, 10) catch { + return CliArgs{ .problem = ArgProblem{ .invalid_flag_value = .{ .flag = "--wasm-stack-size", .value = value, .valid_options = "positive integer (bytes)" } } }; + }; + } else { + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--wasm-stack-size" } } }; + } + } else if (mem.eql(u8, arg, "--z-dump-linker")) { + z_dump_linker = true; } else { if (path != null) { - return CliArgs{ .problem = CliProblem{ .unexpected_argument = .{ .cmd = "build", .arg = arg } } }; + return CliArgs{ .problem = ArgProblem{ .unexpected_argument = .{ .cmd = "build", .arg = arg } } }; } path = arg; } } - return CliArgs{ .build = BuildArgs{ .path = path orelse "main.roc", .opt = opt, .output = output, .z_bench_tokenize = z_bench_tokenize, .z_bench_parse = z_bench_parse } }; + return CliArgs{ .build = BuildArgs{ .path = path orelse "main.roc", .opt = opt, .target = target, .output = output, .debug = debug, .allow_errors = allow_errors, .wasm_memory = wasm_memory, .wasm_stack_size = wasm_stack_size, .z_bench_tokenize = z_bench_tokenize, .z_bench_parse = z_bench_parse, .z_dump_linker = z_dump_linker } }; } -fn parseBundle(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.Error!CliArgs { - var paths = std.ArrayList([]const u8).init(gpa); +fn parseBundle(alloc: mem.Allocator, args: []const []const u8) std.mem.Allocator.Error!CliArgs { + var paths = try std.array_list.Managed([]const u8).initCapacity(alloc, 16); var output_dir: ?[]const u8 = null; var compression_level: i32 = 3; @@ -305,27 +390,27 @@ fn parseBundle(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.E } else if (mem.eql(u8, arg, "--output-dir")) { if (i + 1 >= args.len) { paths.deinit(); - return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--output-dir" } } }; + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--output-dir" } } }; } i += 1; output_dir = args[i]; } else if (mem.eql(u8, arg, "--compression")) { if (i + 1 >= args.len) { paths.deinit(); - return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--compression" } } }; + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--compression" } } }; } i += 1; compression_level = std.fmt.parseInt(i32, args[i], 10) catch { paths.deinit(); - return CliArgs{ .problem = CliProblem{ .invalid_flag_value = .{ .value = args[i], .flag = "--compression", .valid_options = "integer between 1 and 22" } } }; + return CliArgs{ .problem = ArgProblem{ .invalid_flag_value = .{ .value = args[i], .flag = "--compression", .valid_options = "integer between 1 and 22" } } }; }; if (compression_level < 1 or compression_level > 22) { paths.deinit(); - return CliArgs{ .problem = CliProblem{ .invalid_flag_value = .{ .value = args[i], .flag = "--compression", .valid_options = "integer between 1 and 22" } } }; + return CliArgs{ .problem = ArgProblem{ .invalid_flag_value = .{ .value = args[i], .flag = "--compression", .valid_options = "integer between 1 and 22" } } }; } } else if (mem.startsWith(u8, arg, "--")) { paths.deinit(); - return CliArgs{ .problem = CliProblem{ .unexpected_argument = .{ .cmd = "bundle", .arg = arg } } }; + return CliArgs{ .problem = ArgProblem{ .unexpected_argument = .{ .cmd = "bundle", .arg = arg } } }; } else { try paths.append(arg); } @@ -343,8 +428,8 @@ fn parseBundle(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.E } }; } -fn parseUnbundle(gpa: mem.Allocator, args: []const []const u8) !CliArgs { - var paths = std.ArrayList([]const u8).init(gpa); +fn parseUnbundle(alloc: mem.Allocator, args: []const []const u8) !CliArgs { + var paths = try std.array_list.Managed([]const u8).initCapacity(alloc, 16); for (args) |arg| { if (isHelpFlag(arg)) { @@ -364,7 +449,7 @@ fn parseUnbundle(gpa: mem.Allocator, args: []const []const u8) !CliArgs { }; } else if (mem.startsWith(u8, arg, "-")) { paths.deinit(); - return CliArgs{ .problem = CliProblem{ .unexpected_argument = .{ .cmd = "unbundle", .arg = arg } } }; + return CliArgs{ .problem = ArgProblem{ .unexpected_argument = .{ .cmd = "unbundle", .arg = arg } } }; } else { try paths.append(arg); } @@ -377,7 +462,7 @@ fn parseUnbundle(gpa: mem.Allocator, args: []const []const u8) !CliArgs { var iter = cwd.iterate(); while (try iter.next()) |entry| { if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".tar.zst")) { - try paths.append(try gpa.dupe(u8, entry.name)); + try paths.append(try alloc.dupe(u8, entry.name)); } } @@ -407,8 +492,8 @@ fn parseUnbundle(gpa: mem.Allocator, args: []const []const u8) !CliArgs { } }; } -fn parseFormat(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.Error!CliArgs { - var paths = std.ArrayList([]const u8).init(gpa); +fn parseFormat(alloc: mem.Allocator, args: []const []const u8) std.mem.Allocator.Error!CliArgs { + var paths = try std.array_list.Managed([]const u8).initCapacity(alloc, 16); var stdin = false; var check = false; for (args) |arg| { @@ -418,7 +503,7 @@ fn parseFormat(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.E return CliArgs{ .help = \\Format a .roc file or the .roc files contained in a directory using standard Roc formatting \\ - \\Usage: roc format [OPTIONS] [DIRECTORY_OR_FILES] + \\Usage: roc fmt [OPTIONS] [DIRECTORY_OR_FILES] \\ \\Arguments: \\ [DIRECTORY_OR_FILES] @@ -443,7 +528,7 @@ fn parseFormat(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.E if (paths.items.len == 0) { try paths.append("main.roc"); } - return CliArgs{ .format = FormatArgs{ .paths = try paths.toOwnedSlice(), .stdin = stdin, .check = check } }; + return CliArgs{ .fmt = FormatArgs{ .paths = try paths.toOwnedSlice(), .stdin = stdin, .check = check } }; } fn parseTest(args: []const []const u8) CliArgs { @@ -472,23 +557,23 @@ fn parseTest(args: []const []const u8) CliArgs { if (getFlagValue(arg)) |value| { main = value; } else { - return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--main" } } }; + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--main" } } }; } } else if (mem.startsWith(u8, arg, "--opt")) { if (getFlagValue(arg)) |value| { if (OptLevel.from_str(value)) |level| { opt = level; } else { - return CliArgs{ .problem = CliProblem{ .invalid_flag_value = .{ .flag = "--opt", .value = value, .valid_options = "speed,size,dev" } } }; + return CliArgs{ .problem = ArgProblem{ .invalid_flag_value = .{ .flag = "--opt", .value = value, .valid_options = "speed,size,dev" } } }; } } else { - return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--opt" } } }; + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--opt" } } }; } } else if (mem.eql(u8, arg, "--verbose")) { verbose = true; } else { if (path != null) { - return CliArgs{ .problem = CliProblem{ .unexpected_argument = .{ .cmd = "test", .arg = arg } } }; + return CliArgs{ .problem = ArgProblem{ .unexpected_argument = .{ .cmd = "test", .arg = arg } } }; } path = arg; } @@ -509,7 +594,7 @@ fn parseRepl(args: []const []const u8) CliArgs { \\ }; } else { - return CliArgs{ .problem = CliProblem{ .unexpected_argument = .{ .cmd = "repl", .arg = arg } } }; + return CliArgs{ .problem = ArgProblem{ .unexpected_argument = .{ .cmd = "repl", .arg = arg } } }; } } return CliArgs.repl; @@ -528,7 +613,7 @@ fn parseVersion(args: []const []const u8) CliArgs { \\ }; } else { - return CliArgs{ .problem = CliProblem{ .unexpected_argument = .{ .cmd = "version", .arg = arg } } }; + return CliArgs{ .problem = ArgProblem{ .unexpected_argument = .{ .cmd = "version", .arg = arg } } }; } } return CliArgs.version; @@ -547,16 +632,21 @@ fn parseLicenses(args: []const []const u8) CliArgs { \\ }; } else { - return CliArgs{ .problem = CliProblem{ .unexpected_argument = .{ .cmd = "licenses", .arg = arg } } }; + return CliArgs{ .problem = ArgProblem{ .unexpected_argument = .{ .cmd = "licenses", .arg = arg } } }; } } return CliArgs.licenses; } fn parseDocs(args: []const []const u8) CliArgs { - var output: ?[]const u8 = null; - var root_dir: ?[]const u8 = null; var path: ?[]const u8 = null; + var main: ?[]const u8 = null; + var output: ?[]const u8 = null; + var time: bool = false; + var no_cache: bool = false; + var verbose: bool = false; + var serve: bool = false; + for (args) |arg| { if (isHelpFlag(arg)) { return CliArgs{ .help = @@ -565,43 +655,113 @@ fn parseDocs(args: []const []const u8) CliArgs { \\Usage: roc docs [OPTIONS] [ROC_FILE] \\ \\Arguments: - \\ [ROC_FILE] The package's main .roc file [default: main.roc] + \\ [ROC_FILE] The .roc file to generate docs for [default: main.roc] \\ \\Options: - \\ --output= Output directory for the generated documentation files. [default: generated-docs] - \\ --root-dir= Set a root directory path to be used as a prefix for URL links in the generated documentation files. - \\ -h, --help Print help + \\ --main=
The .roc file of the main app/package module to resolve dependencies from + \\ --output= Output directory for generated documentation [default: generated-docs] + \\ --serve Start an HTTP server to view the documentation + \\ --time Print timing information for each compilation phase. Will not print anything if everything is cached. + \\ --no-cache Disable caching + \\ --verbose Enable verbose output including cache statistics + \\ -h, --help Print help \\ }; + } else if (mem.startsWith(u8, arg, "--main")) { + if (getFlagValue(arg)) |value| { + main = value; + } else { + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--main" } } }; + } } else if (mem.startsWith(u8, arg, "--output")) { if (getFlagValue(arg)) |value| { output = value; } else { - return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--output" } } }; - } - } else if (mem.startsWith(u8, arg, "--root-dir")) { - if (getFlagValue(arg)) |value| { - root_dir = value; - } else { - return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--root-dir" } } }; + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--output" } } }; } + } else if (mem.eql(u8, arg, "--serve")) { + serve = true; + } else if (mem.eql(u8, arg, "--time")) { + time = true; + } else if (mem.eql(u8, arg, "--no-cache")) { + no_cache = true; + } else if (mem.eql(u8, arg, "--verbose")) { + verbose = true; } else { if (path != null) { - return CliArgs{ .problem = CliProblem{ .unexpected_argument = .{ .cmd = "docs", .arg = arg } } }; + return CliArgs{ .problem = ArgProblem{ .unexpected_argument = .{ .cmd = "docs", .arg = arg } } }; } path = arg; } } - return CliArgs{ .docs = DocsArgs{ .path = path orelse "main.roc", .output = output orelse "generated-docs", .root_dir = root_dir } }; + return CliArgs{ .docs = DocsArgs{ .path = path orelse "main.roc", .main = main, .output = output orelse "generated-docs", .time = time, .no_cache = no_cache, .verbose = verbose, .serve = serve } }; } -fn parseRun(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.Error!CliArgs { +fn parseExperimentalLsp(args: []const []const u8) CliArgs { + var debug_io = false; + var debug_build = false; + var debug_syntax = false; + var debug_server = false; + + for (args) |arg| { + if (isHelpFlag(arg)) { + return CliArgs{ .help = + \\Start the experimental Roc language server (LSP) + \\ + \\Usage: roc experimental-lsp [OPTIONS] + \\ + \\Options: + \\ --debug-transport Mirror all JSON-RPC traffic to a temp log file + \\ --debug-build Log build environment actions to the debug log + \\ --debug-syntax Log syntax/type checking steps to the debug log + \\ --debug-server Log server lifecycle details to the debug log + \\ -h, --help Print help + \\ + }; + } else if (mem.eql(u8, arg, "--debug-transport")) { + debug_io = true; + } else if (mem.eql(u8, arg, "--debug-build")) { + debug_build = true; + } else if (mem.eql(u8, arg, "--debug-syntax")) { + debug_syntax = true; + } else if (mem.eql(u8, arg, "--debug-server")) { + debug_server = true; + } else { + return CliArgs{ .problem = ArgProblem{ .unexpected_argument = .{ .cmd = "experimental-lsp", .arg = arg } } }; + } + } + + return CliArgs{ .experimental_lsp = .{ + .debug_io = debug_io, + .debug_build = debug_build, + .debug_syntax = debug_syntax, + .debug_server = debug_server, + } }; +} + +fn parseRun(alloc: mem.Allocator, args: []const []const u8) std.mem.Allocator.Error!CliArgs { var path: ?[]const u8 = null; var opt: OptLevel = .dev; + var target: ?[]const u8 = null; var no_cache: bool = false; - var app_args = std.ArrayList([]const u8).init(gpa); + var allow_errors: bool = false; + var app_args = try std.array_list.Managed([]const u8).initCapacity(alloc, 16); + var past_double_dash = false; + for (args) |arg| { + // After "--", all remaining args go to the app (no flag processing) + if (past_double_dash) { + try app_args.append(arg); + continue; + } + + // Check for "--" separator + if (mem.eql(u8, arg, "--")) { + past_double_dash = true; + continue; + } + if (isHelpFlag(arg)) { // We need to free the paths here because we aren't returning the .run variant app_args.deinit(); @@ -610,18 +770,29 @@ fn parseRun(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.Erro // We need to free the paths here because we aren't returning the .format variant app_args.deinit(); return CliArgs.version; + } else if (mem.startsWith(u8, arg, "--target")) { + if (getFlagValue(arg)) |value| { + target = value; + } else { + app_args.deinit(); + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--target" } } }; + } } else if (mem.startsWith(u8, arg, "--opt")) { if (getFlagValue(arg)) |value| { if (OptLevel.from_str(value)) |level| { opt = level; } else { - return CliArgs{ .problem = CliProblem{ .invalid_flag_value = .{ .flag = "--opt", .value = value, .valid_options = "speed,size,dev" } } }; + app_args.deinit(); + return CliArgs{ .problem = ArgProblem{ .invalid_flag_value = .{ .flag = "--opt", .value = value, .valid_options = "speed,size,dev" } } }; } } else { - return CliArgs{ .problem = CliProblem{ .missing_flag_value = .{ .flag = "--opt" } } }; + app_args.deinit(); + return CliArgs{ .problem = ArgProblem{ .missing_flag_value = .{ .flag = "--opt" } } }; } } else if (mem.eql(u8, arg, "--no-cache")) { no_cache = true; + } else if (mem.eql(u8, arg, "--allow-errors")) { + allow_errors = true; } else { if (path != null) { try app_args.append(arg); @@ -630,7 +801,7 @@ fn parseRun(gpa: mem.Allocator, args: []const []const u8) std.mem.Allocator.Erro } } } - return CliArgs{ .run = RunArgs{ .path = path orelse "main.roc", .opt = opt, .app_args = try app_args.toOwnedSlice(), .no_cache = no_cache } }; + return CliArgs{ .run = RunArgs{ .path = path orelse "main.roc", .opt = opt, .target = target, .app_args = try app_args.toOwnedSlice(), .no_cache = no_cache, .allow_errors = allow_errors } }; } fn isHelpFlag(arg: []const u8) bool { @@ -706,6 +877,48 @@ test "roc run" { defer result.deinit(gpa); try testing.expectEqualStrings("notreal", result.problem.invalid_flag_value.value); } + // Test -- separator: args after -- should go to app_args + { + const result = try parse(gpa, &[_][]const u8{ "foo.roc", "--", "arg1", "arg2" }); + defer result.deinit(gpa); + try testing.expectEqualStrings("foo.roc", result.run.path); + try testing.expectEqual(@as(usize, 2), result.run.app_args.len); + try testing.expectEqualStrings("arg1", result.run.app_args[0]); + try testing.expectEqualStrings("arg2", result.run.app_args[1]); + } + // Test -- separator is not included in app_args + { + const result = try parse(gpa, &[_][]const u8{ "foo.roc", "--", "onlyarg" }); + defer result.deinit(gpa); + try testing.expectEqual(@as(usize, 1), result.run.app_args.len); + try testing.expectEqualStrings("onlyarg", result.run.app_args[0]); + } + // Test flags after -- are treated as app args, not roc flags + { + const result = try parse(gpa, &[_][]const u8{ "foo.roc", "--", "--help", "-v", "--version" }); + defer result.deinit(gpa); + try testing.expectEqual(.run, std.meta.activeTag(result)); + try testing.expectEqual(@as(usize, 3), result.run.app_args.len); + try testing.expectEqualStrings("--help", result.run.app_args[0]); + try testing.expectEqualStrings("-v", result.run.app_args[1]); + try testing.expectEqualStrings("--version", result.run.app_args[2]); + } + // Test -- with flags before it still parses roc flags + { + const result = try parse(gpa, &[_][]const u8{ "--opt=speed", "foo.roc", "--", "arg1" }); + defer result.deinit(gpa); + try testing.expectEqualStrings("foo.roc", result.run.path); + try testing.expectEqual(.speed, result.run.opt); + try testing.expectEqual(@as(usize, 1), result.run.app_args.len); + try testing.expectEqualStrings("arg1", result.run.app_args[0]); + } + // Test -- without any args after it + { + const result = try parse(gpa, &[_][]const u8{ "foo.roc", "--" }); + defer result.deinit(gpa); + try testing.expectEqualStrings("foo.roc", result.run.path); + try testing.expectEqual(@as(usize, 0), result.run.app_args.len); + } } test "roc build" { @@ -760,6 +973,19 @@ test "roc build" { defer result.deinit(gpa); try testing.expectEqualStrings("bar.roc", result.problem.unexpected_argument.arg); } + { + // Test --debug flag + const result = try parse(gpa, &[_][]const u8{ "build", "--debug", "foo.roc" }); + defer result.deinit(gpa); + try testing.expectEqualStrings("foo.roc", result.build.path); + try testing.expect(result.build.debug); + } + { + // Test that debug defaults to false + const result = try parse(gpa, &[_][]const u8{ "build", "foo.roc" }); + defer result.deinit(gpa); + try testing.expect(!result.build.debug); + } { const result = try parse(gpa, &[_][]const u8{ "build", "-h" }); defer result.deinit(gpa); @@ -782,61 +1008,61 @@ test "roc build" { } } -test "roc format" { +test "roc fmt" { const gpa = testing.allocator; { - const result = try parse(gpa, &[_][]const u8{"format"}); + const result = try parse(gpa, &[_][]const u8{"fmt"}); defer result.deinit(gpa); - try testing.expectEqualStrings("main.roc", result.format.paths[0]); - try testing.expect(!result.format.stdin); - try testing.expect(!result.format.check); + try testing.expectEqualStrings("main.roc", result.fmt.paths[0]); + try testing.expect(!result.fmt.stdin); + try testing.expect(!result.fmt.check); } { - const result = try parse(gpa, &[_][]const u8{ "format", "--check" }); + const result = try parse(gpa, &[_][]const u8{ "fmt", "--check" }); defer result.deinit(gpa); - try testing.expectEqualStrings("main.roc", result.format.paths[0]); - try testing.expect(!result.format.stdin); - try testing.expect(result.format.check); + try testing.expectEqualStrings("main.roc", result.fmt.paths[0]); + try testing.expect(!result.fmt.stdin); + try testing.expect(result.fmt.check); } { - const result = try parse(gpa, &[_][]const u8{ "format", "--stdin" }); + const result = try parse(gpa, &[_][]const u8{ "fmt", "--stdin" }); defer result.deinit(gpa); - try testing.expectEqualStrings("main.roc", result.format.paths[0]); - try testing.expect(result.format.stdin); - try testing.expect(!result.format.check); + try testing.expectEqualStrings("main.roc", result.fmt.paths[0]); + try testing.expect(result.fmt.stdin); + try testing.expect(!result.fmt.check); } { - const result = try parse(gpa, &[_][]const u8{ "format", "--stdin", "--check", "foo.roc" }); + const result = try parse(gpa, &[_][]const u8{ "fmt", "--stdin", "--check", "foo.roc" }); defer result.deinit(gpa); - try testing.expectEqualStrings("foo.roc", result.format.paths[0]); - try testing.expect(result.format.stdin); - try testing.expect(result.format.check); + try testing.expectEqualStrings("foo.roc", result.fmt.paths[0]); + try testing.expect(result.fmt.stdin); + try testing.expect(result.fmt.check); } { - const result = try parse(gpa, &[_][]const u8{ "format", "foo.roc", "bar.roc" }); + const result = try parse(gpa, &[_][]const u8{ "fmt", "foo.roc", "bar.roc" }); defer result.deinit(gpa); - try testing.expectEqualStrings("foo.roc", result.format.paths[0]); - try testing.expectEqualStrings("bar.roc", result.format.paths[1]); + try testing.expectEqualStrings("foo.roc", result.fmt.paths[0]); + try testing.expectEqualStrings("bar.roc", result.fmt.paths[1]); } { - const result = try parse(gpa, &[_][]const u8{ "format", "-h" }); + const result = try parse(gpa, &[_][]const u8{ "fmt", "-h" }); defer result.deinit(gpa); try testing.expectEqual(.help, std.meta.activeTag(result)); } { - const result = try parse(gpa, &[_][]const u8{ "format", "--help" }); + const result = try parse(gpa, &[_][]const u8{ "fmt", "--help" }); defer result.deinit(gpa); try testing.expectEqual(.help, std.meta.activeTag(result)); } { - const result = try parse(gpa, &[_][]const u8{ "format", "foo.roc", "--help" }); + const result = try parse(gpa, &[_][]const u8{ "fmt", "foo.roc", "--help" }); defer result.deinit(gpa); try testing.expectEqual(.help, std.meta.activeTag(result)); } { - const result = try parse(gpa, &[_][]const u8{ "format", "--thisisactuallyafile" }); + const result = try parse(gpa, &[_][]const u8{ "fmt", "--thisisactuallyafile" }); defer result.deinit(gpa); - try testing.expectEqualStrings("--thisisactuallyafile", result.format.paths[0]); + try testing.expectEqualStrings("--thisisactuallyafile", result.fmt.paths[0]); } } @@ -1000,20 +1226,34 @@ test "roc docs" { const result = try parse(gpa, &[_][]const u8{"docs"}); defer result.deinit(gpa); try testing.expectEqualStrings("main.roc", result.docs.path); + try testing.expectEqual(null, result.docs.main); try testing.expectEqualStrings("generated-docs", result.docs.output); - try testing.expectEqual(null, result.docs.root_dir); + try testing.expectEqual(false, result.docs.time); + try testing.expectEqual(false, result.docs.no_cache); + try testing.expectEqual(false, result.docs.verbose); } { - const result = try parse(gpa, &[_][]const u8{ "docs", "foo/bar.roc", "--root-dir=/root/dir", "--output=my_output_dir" }); + const result = try parse(gpa, &[_][]const u8{ "docs", "foo.roc" }); defer result.deinit(gpa); - try testing.expectEqualStrings("foo/bar.roc", result.docs.path); - try testing.expectEqualStrings("my_output_dir", result.docs.output); - try testing.expectEqualStrings("/root/dir", result.docs.root_dir.?); + try testing.expectEqualStrings("foo.roc", result.docs.path); + try testing.expectEqualStrings("generated-docs", result.docs.output); } { - const result = try parse(gpa, &[_][]const u8{ "docs", "foo.roc", "--madeup" }); + const result = try parse(gpa, &[_][]const u8{ "docs", "--main=mymain.roc", "foo.roc" }); defer result.deinit(gpa); - try testing.expectEqualStrings("--madeup", result.problem.unexpected_argument.arg); + try testing.expectEqualStrings("foo.roc", result.docs.path); + try testing.expectEqualStrings("mymain.roc", result.docs.main.?); + } + { + const result = try parse(gpa, &[_][]const u8{ "docs", "--output=my-docs", "foo.roc" }); + defer result.deinit(gpa); + try testing.expectEqualStrings("foo.roc", result.docs.path); + try testing.expectEqualStrings("my-docs", result.docs.output); + } + { + const result = try parse(gpa, &[_][]const u8{ "docs", "foo.roc", "bar.roc" }); + defer result.deinit(gpa); + try testing.expectEqualStrings("bar.roc", result.problem.unexpected_argument.arg); } { const result = try parse(gpa, &[_][]const u8{ "docs", "-h" }); @@ -1030,6 +1270,29 @@ test "roc docs" { defer result.deinit(gpa); try testing.expectEqual(.help, std.meta.activeTag(result)); } + { + const result = try parse(gpa, &[_][]const u8{ "docs", "--time" }); + defer result.deinit(gpa); + try testing.expectEqualStrings("main.roc", result.docs.path); + try testing.expectEqual(true, result.docs.time); + } + { + const result = try parse(gpa, &[_][]const u8{ "docs", "foo.roc", "--time", "--main=bar.roc" }); + defer result.deinit(gpa); + try testing.expectEqualStrings("foo.roc", result.docs.path); + try testing.expectEqualStrings("bar.roc", result.docs.main.?); + try testing.expectEqual(true, result.docs.time); + } + { + const result = try parse(gpa, &[_][]const u8{ "docs", "--no-cache" }); + defer result.deinit(gpa); + try testing.expectEqual(true, result.docs.no_cache); + } + { + const result = try parse(gpa, &[_][]const u8{ "docs", "--verbose" }); + defer result.deinit(gpa); + try testing.expectEqual(true, result.docs.verbose); + } } test "roc help" { @@ -1045,6 +1308,7 @@ test "roc help" { try testing.expectEqual(.help, std.meta.activeTag(result)); } } + test "roc licenses" { const gpa = testing.allocator; { diff --git a/src/cli/cross_compilation.zig b/src/cli/cross_compilation.zig new file mode 100644 index 0000000000..dcc4e4187a --- /dev/null +++ b/src/cli/cross_compilation.zig @@ -0,0 +1,114 @@ +//! Cross-compilation support and validation for Roc CLI +//! Handles target validation and capability matrix + +const std = @import("std"); +const target_mod = @import("target.zig"); + +const RocTarget = target_mod.RocTarget; + +/// Result of cross-compilation validation +pub const CrossCompilationResult = union(enum) { + supported: void, + unsupported_host_target: struct { + host: RocTarget, + reason: []const u8, + }, + unsupported_cross_compilation: struct { + host: RocTarget, + target: RocTarget, + reason: []const u8, + }, + missing_toolchain: struct { + host: RocTarget, + target: RocTarget, + required_tools: []const []const u8, + }, +}; + +/// Cross-compilation capability matrix +pub const CrossCompilationMatrix = struct { + /// Targets that support static linking (musl) - these should work from any host + pub const musl_targets = [_]RocTarget{ + .x64musl, + .arm64musl, + .arm32musl, + }; +}; + +/// Validate cross-compilation from host to target +pub fn validateCrossCompilation(host: RocTarget, target: RocTarget) CrossCompilationResult { + // Native compilation (host == target) is always supported + if (host == target) { + return CrossCompilationResult{ .supported = {} }; + } + + // Support musl targets for cross-compilation (statically linked) + if (target.isStatic()) { + return CrossCompilationResult{ .supported = {} }; + } + + // glibc, Windows and macOS cross-compilation not yet supported + return CrossCompilationResult{ + .unsupported_cross_compilation = .{ + .host = host, + .target = target, + .reason = "Only Linux musl targets (x64musl, arm64musl, arm32musl) are currently supported for cross-compilation. glibc, Windows and macOS support coming in a future release. Log an issue at https://github.com/roc-lang/roc/issues", + }, + }; +} + +/// Get host capabilities (what this host can cross-compile to) +pub fn getHostCapabilities(host: RocTarget) []const RocTarget { + _ = host; // For now, all hosts have the same capabilities + + // Only musl targets are supported for cross-compilation + return &CrossCompilationMatrix.musl_targets; +} + +/// Print supported targets for the current host +pub fn printSupportedTargets(writer: anytype, host: RocTarget) !void { + const capabilities = getHostCapabilities(host); + + try writer.print("Supported cross-compilation targets from {s}:\n", .{@tagName(host)}); + for (capabilities) |target| { + try writer.print(" {s} ({s})\n", .{ @tagName(target), target.toTriple() }); + } + + try writer.print("\nUnsupported targets (not yet implemented):\n", .{}); + const unsupported = [_][]const u8{ + "x64glibc, arm64glibc (Linux glibc cross-compilation)", + "x64windows, arm64windows (Windows cross-compilation)", + "x64macos, arm64macos (macOS cross-compilation)", + }; + + for (unsupported) |target_desc| { + try writer.print(" {s}\n", .{target_desc}); + } + + try writer.print("\nTo request support for additional targets, please log an issue at:\n", .{}); + try writer.print("https://github.com/roc-lang/roc/issues\n", .{}); +} + +/// Print cross-compilation error with helpful context +pub fn printCrossCompilationError(writer: anytype, result: CrossCompilationResult) !void { + switch (result) { + .supported => {}, // No error + .unsupported_host_target => |info| { + try writer.print("Error: Unsupported host platform '{s}'\n", .{@tagName(info.host)}); + try writer.print("Reason: {s}\n", .{info.reason}); + }, + .unsupported_cross_compilation => |info| { + try writer.print("Error: Cross-compilation from {s} to {s} is not supported\n", .{ @tagName(info.host), @tagName(info.target) }); + try writer.print("Reason: {s}\n", .{info.reason}); + try writer.print("\n", .{}); + try printSupportedTargets(writer, info.host); + }, + .missing_toolchain => |info| { + try writer.print("Error: Missing required toolchain for cross-compilation from {s} to {s}\n", .{ @tagName(info.host), @tagName(info.target) }); + try writer.print("Required tools:\n", .{}); + for (info.required_tools) |tool| { + try writer.print(" {s}\n", .{tool}); + } + }, + } +} diff --git a/src/cli/libc_finder.zig b/src/cli/libc_finder.zig new file mode 100644 index 0000000000..b894841910 --- /dev/null +++ b/src/cli/libc_finder.zig @@ -0,0 +1,350 @@ +//! Finds libc and dynamic linker paths on Linux systems +//! +//! Only used when building natively (not cross-compiling for another target) +//! +//! TODO can we improve this or make it more reliable? this implementation probably +//! needs some work but it will be hard to know until we have more users testing roc +//! on different systems. + +const std = @import("std"); +const builtin = @import("builtin"); +const base = @import("base"); +const Allocators = base.Allocators; +const cli_ctx = @import("CliContext.zig"); +const CliContext = cli_ctx.CliContext; +const Io = cli_ctx.Io; +const fs = std.fs; +const process = std.process; + +/// Information about the system's libc installation +pub const LibcInfo = struct { + /// Path to the dynamic linker (e.g., /lib64/ld-linux-x86-64.so.2) + dynamic_linker: []const u8, + + /// Path to libc library (e.g., /lib/x86_64-linux-gnu/libc.so.6) + libc_path: []const u8, + + /// Directory containing libc and CRT files + lib_dir: []const u8, + + /// System architecture (e.g., "x86_64", "aarch64") + arch: []const u8, +}; + +/// Validate that a path is safe (absolute and no traversal) +fn validatePath(path: []const u8) bool { + if (!fs.path.isAbsolute(path)) return false; + if (std.mem.indexOf(u8, path, "../") != null) return false; + return true; +} + +/// Get the dynamic linker name for the given architecture +fn getDynamicLinkerName(arch: []const u8) []const u8 { + if (std.mem.eql(u8, arch, "x86_64")) { + return "ld-linux-x86-64.so.2"; + } else if (std.mem.eql(u8, arch, "aarch64")) { + return "ld-linux-aarch64.so.1"; + } else if (std.mem.startsWith(u8, arch, "arm")) { + return "ld-linux-armhf.so.3"; + } else if (std.mem.eql(u8, arch, "i686") or std.mem.eql(u8, arch, "i386")) { + return "ld-linux.so.2"; + } else { + return "ld-linux.so.2"; + } +} + +/// finds libc and dynamic linker +/// Solely allocates into the arena +pub fn findLibc(ctx: *CliContext) !LibcInfo { + // Try compiler-based detection first (most reliable) + if (try findViaCompiler(ctx.arena)) |info| + return info + else + // Fall back to filesystem search + return try findViaFilesystem(ctx.arena); +} + +/// Find libc using compiler queries (gcc/clang) +fn findViaCompiler(arena: std.mem.Allocator) !?LibcInfo { + const compilers = [_][]const u8{ "gcc", "clang", "cc" }; + + // Get architecture first + const arch = try getArchitecture(arena); + + // Get the expected dynamic linker name for this architecture + const ld_name = getDynamicLinkerName(arch); + + for (compilers) |compiler| { + // Try to get dynamic linker path from compiler + const ld_cmd = try std.fmt.allocPrint(arena, "-print-file-name={s}", .{ld_name}); + + // TODO: Do we need to do something with this process' stdout, + // or is this only here to continue to the next iteration? + // Could be that it was forgotten before I refactored it and now to intent is lost. + _ = process.Child.run(.{ + .allocator = arena, + .argv = &[_][]const u8{ compiler, ld_cmd }, + }) catch continue; + + // Try to get libc path from compiler + const libc_result = process.Child.run(.{ + .allocator = arena, + .argv = &[_][]const u8{ compiler, "-print-file-name=libc.so" }, + }) catch continue; + + const libc_path = std.mem.trimRight(u8, libc_result.stdout, "\n\r \t"); + if (libc_path.len == 0 or std.mem.eql(u8, libc_path, "libc.so")) continue; + + // Validate path for security + if (!validatePath(libc_path)) continue; + + // Verify the file exists and close it properly + const libc_file = fs.openFileAbsolute(libc_path, .{}) catch continue; + libc_file.close(); + + const lib_dir = fs.path.dirname(libc_path) orelse continue; + + // Find dynamic linker + const dynamic_linker = try findDynamicLinker(arena, arch, lib_dir) orelse continue; + + // Validate dynamic linker path + if (!validatePath(dynamic_linker)) continue; + + return LibcInfo{ + .dynamic_linker = dynamic_linker, + .libc_path = libc_path, + .lib_dir = lib_dir, + .arch = arch, + }; + } + + return null; +} + +/// Find libc by searching the filesystem +fn findViaFilesystem(arena: std.mem.Allocator) !LibcInfo { + const arch = try getArchitecture(arena); + const search_paths = try getSearchPaths(arena, arch); + + // Search for libc in standard paths + for (search_paths) |lib_dir| { + var dir = fs.openDirAbsolute(lib_dir, .{}) catch continue; + defer dir.close(); + + // Support both glibc and musl + const libc_names = [_][]const u8{ + "libc.so.6", // glibc + "libc.musl-x86_64.so.1", // musl x86_64 + "libc.musl-aarch64.so.1", // musl aarch64 + "libc.musl-arm.so.1", // musl arm + "libc.so", + "libc.a", + }; + + for (libc_names) |libc_name| { + const libc_path = try fs.path.join(arena, &[_][]const u8{ lib_dir, libc_name }); + + // Check if file exists and close it properly + const libc_file = fs.openFileAbsolute(libc_path, .{}) catch continue; + libc_file.close(); + + // Try to find dynamic linker + const dynamic_linker = try findDynamicLinker(arena, arch, lib_dir) orelse continue; + + // Validate paths for security + if (!validatePath(libc_path) or !validatePath(dynamic_linker)) { + continue; + } + + return LibcInfo{ + .lib_dir = lib_dir, + .dynamic_linker = dynamic_linker, + .libc_path = libc_path, + .arch = arch, + }; + } + } + + return error.LibcNotFound; +} + +/// Find the dynamic linker for the given architecture +fn findDynamicLinker(arena: std.mem.Allocator, arch: []const u8, lib_dir: []const u8) !?[]const u8 { + // Map architecture to dynamic linker names (including musl) + const ld_names = if (std.mem.eql(u8, arch, "x86_64")) + &[_][]const u8{ "ld-linux-x86-64.so.2", "ld-musl-x86_64.so.1", "ld-linux.so.2" } + else if (std.mem.eql(u8, arch, "aarch64")) + &[_][]const u8{ "ld-linux-aarch64.so.1", "ld-musl-aarch64.so.1", "ld-linux.so.1" } + else if (std.mem.startsWith(u8, arch, "arm")) + &[_][]const u8{ "ld-linux-armhf.so.3", "ld-musl-arm.so.1", "ld-linux.so.3" } + else if (std.mem.eql(u8, arch, "i686") or std.mem.eql(u8, arch, "i386")) + &[_][]const u8{ "ld-linux.so.2", "ld-musl-i386.so.1" } + else + &[_][]const u8{ "ld-linux.so.2", "ld.so.1" }; + + // Search in the lib directory first + for (ld_names) |ld_name| { + const path = try fs.path.join(arena, &[_][]const u8{ lib_dir, ld_name }); + + if (fs.openFileAbsolute(path, .{})) |file| { + file.close(); + return path; + } else |_| {} + } + + // Search in common locations + const common_paths = if (std.mem.eql(u8, arch, "x86_64")) + &[_][]const u8{ "/lib64", "/lib/x86_64-linux-gnu", "/lib" } + else if (std.mem.eql(u8, arch, "aarch64")) + &[_][]const u8{ "/lib", "/lib/aarch64-linux-gnu", "/lib64" } + else if (std.mem.startsWith(u8, arch, "arm")) + &[_][]const u8{ "/lib", "/lib/arm-linux-gnueabihf", "/lib32" } + else + &[_][]const u8{ "/lib", "/lib32" }; + + for (common_paths) |search_dir| { + for (ld_names) |ld_name| { + const path = try fs.path.join(arena, &[_][]const u8{ search_dir, ld_name }); + + if (fs.openFileAbsolute(path, .{})) |file| { + file.close(); + return path; + } else |_| {} + } + } + + return null; +} + +/// Get system architecture using uname +fn getArchitecture(arena: std.mem.Allocator) ![]const u8 { + const result = try process.Child.run(.{ + .allocator = arena, + .argv = &[_][]const u8{ "uname", "-m" }, + }); + + return std.mem.trimRight(u8, result.stdout, "\n\r \t"); +} + +/// Get library search paths for the given architecture +fn getSearchPaths(arena: std.mem.Allocator, arch: []const u8) ![]const []const u8 { + const triplet = getMultiarchTriplet(arena, arch) catch |err| blk: { + switch (err) { + error.UnrecognisedArch => break :blk arch, + else => |other_err| return other_err, + } + }; + + // Dynamic string allocations for multiarch locations + const path_lib_triplet = try std.fmt.allocPrint(arena, "/lib/{s}", .{triplet}); + const path_usr_lib_triplet = try std.fmt.allocPrint(arena, "/usr/lib/{s}", .{triplet}); + + const arch_paths = if (std.mem.eql(u8, arch, "x86_64")) + &[_][]const u8{ + "/lib64", + "/usr/lib64", + "/lib/x86_64-linux-gnu", + "/usr/lib/x86_64-linux-gnu", + } + else if (std.mem.eql(u8, arch, "aarch64")) + &[_][]const u8{ + "/lib64", + "/usr/lib64", + "/lib/aarch64-linux-gnu", + "/usr/lib/aarch64-linux-gnu", + } + else if (std.mem.startsWith(u8, arch, "arm")) + &[_][]const u8{ + "/lib32", + "/usr/lib32", + "/lib/arm-linux-gnueabihf", + "/usr/lib/arm-linux-gnueabihf", + } + else + &[_][]const u8{}; + + // Always include these generic/musl paths + const root_bases = [_][]const u8{ + path_lib_triplet, + path_usr_lib_triplet, + "/lib", + "/usr/lib", + "/usr/local/lib", + "/lib/musl", + "/usr/lib/musl", + }; + + const total_len = root_bases.len + arch_paths.len; + const result = try arena.alloc([]const u8, total_len); + @memcpy(result[0..root_bases.len], root_bases[0..]); + @memcpy(result[root_bases.len..], arch_paths[0..]); + + return result; +} + +/// Get multiarch triplet (e.g., x86_64-linux-gnu) +fn getMultiarchTriplet(arena: std.mem.Allocator, arch: []const u8) ![]const u8 { + // Try to get from gcc first + const result = process.Child.run(.{ + .allocator = arena, + .argv = &[_][]const u8{ "gcc", "-dumpmachine" }, + }) catch |err| switch (err) { + error.FileNotFound => { + // Fallback to common triplets + if (std.mem.eql(u8, arch, "x86_64")) { + return "x86_64-linux-gnu"; + } else if (std.mem.eql(u8, arch, "aarch64")) { + return "aarch64-linux-gnu"; + } else if (std.mem.startsWith(u8, arch, "arm")) { + return "arm-linux-gnueabihf"; + } else { + return error.UnrecognisedArch; + } + }, + else => return err, + }; + + return std.mem.trimRight(u8, result.stdout, "\n\r \t"); +} + +test "libc detection integration test" { + // This test is not relevant on Windows (`uname` not available) + if (builtin.os.tag == .windows) return error.SkipZigTest; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var io = Io.init(); + var ctx = CliContext.init(std.testing.allocator, arena.allocator(), &io, .build); + ctx.initIo(); + defer ctx.deinit(); + + const libc_info = findLibc(&ctx) catch |err| switch (err) { + error.LibcNotFound => return, + else => return err, + }; + + // Verify we got valid information + try std.testing.expect(libc_info.arch.len > 0); + try std.testing.expect(libc_info.dynamic_linker.len > 0); + try std.testing.expect(libc_info.libc_path.len > 0); + try std.testing.expect(libc_info.lib_dir.len > 0); + + // Verify paths are valid + try std.testing.expect(validatePath(libc_info.dynamic_linker)); + try std.testing.expect(validatePath(libc_info.libc_path)); + + // Verify the dynamic linker file exists and is accessible + const ld_file = fs.openFileAbsolute(libc_info.dynamic_linker, .{}) catch |err| { + std.log.err("Dynamic linker not accessible at {s}: {}", .{ libc_info.dynamic_linker, err }); + return err; + }; + ld_file.close(); + + // Verify the libc file exists and is accessible + const libc_file = fs.openFileAbsolute(libc_info.libc_path, .{}) catch |err| { + std.log.err("Libc not accessible at {s}: {}", .{ libc_info.libc_path, err }); + return err; + }; + libc_file.close(); +} diff --git a/src/cli/linker.zig b/src/cli/linker.zig index 576dc652d1..0a255eedb0 100644 --- a/src/cli/linker.zig +++ b/src/cli/linker.zig @@ -4,7 +4,15 @@ const std = @import("std"); const builtin = @import("builtin"); +const build_options = @import("build_options"); const Allocator = std.mem.Allocator; +const base = @import("base"); +const Allocators = base.Allocators; +const libc_finder = @import("libc_finder.zig"); +const RocTarget = @import("roc_target").RocTarget; +const cli_ctx = @import("CliContext.zig"); +const CliContext = cli_ctx.CliContext; +const Io = cli_ctx.Io; /// External C functions from zig_llvm.cpp - only available when LLVM is enabled const llvm_available = if (@import("builtin").is_test) false else @import("config").llvm; @@ -29,23 +37,65 @@ pub const TargetFormat = enum { return switch (builtin.target.os.tag) { .windows => .coff, .macos, .ios, .watchos, .tvos => .macho, - .wasi => .wasm, + .freestanding => .wasm, + else => .elf, + }; + } + + /// Detect target format from OS tag + pub fn detectFromOs(os: std.Target.Os.Tag) TargetFormat { + return switch (os) { + .windows => .coff, + .macos, .ios, .watchos, .tvos => .macho, + .freestanding => .wasm, else => .elf, }; } }; -/// Configuration for linking operation +/// Target ABI for runtime-configurable linking +pub const TargetAbi = enum { + musl, + gnu, + + /// Convert from RocTarget to TargetAbi + pub fn fromRocTarget(roc_target: RocTarget) TargetAbi { + return if (roc_target.isStatic()) .musl else .gnu; + } +}; + +/// Default WASM initial memory: 64MB +pub const DEFAULT_WASM_INITIAL_MEMORY: usize = 64 * 1024 * 1024; + +/// Default WASM stack size: 8MB +pub const DEFAULT_WASM_STACK_SIZE: usize = 8 * 1024 * 1024; + +/// Configuration for the linker, specifying target format, ABI, paths, and linking options. pub const LinkConfig = struct { /// Target format to use for linking target_format: TargetFormat = TargetFormat.detectFromSystem(), + /// Target ABI - determines static vs dynamic linking strategy + target_abi: ?TargetAbi = null, // null means detect from system + + /// Target OS tag - for cross-compilation support + target_os: ?std.Target.Os.Tag = null, // null means detect from system + + /// Target CPU architecture - for cross-compilation support + target_arch: ?std.Target.Cpu.Arch = null, // null means detect from system + /// Output executable path output_path: []const u8, /// Input object files to link object_files: []const []const u8, + /// Platform-provided files to link before object files (e.g., Scrt1.o, crti.o, host.o) + platform_files_pre: []const []const u8 = &.{}, + + /// Platform-provided files to link after object files (e.g., crtn.o) + platform_files_post: []const []const u8 = &.{}, + /// Additional linker flags extra_args: []const []const u8 = &.{}, @@ -54,6 +104,14 @@ pub const LinkConfig = struct { /// Whether to disable linker output disable_output: bool = false, + + /// Initial memory size for WASM targets (bytes). This is the amount of linear memory + /// available to the WASM module at runtime. Must be a multiple of 64KB (WASM page size). + wasm_initial_memory: usize = DEFAULT_WASM_INITIAL_MEMORY, + + /// Stack size for WASM targets (bytes). This is the amount of memory reserved for the + /// call stack within the WASM linear memory. Must be a multiple of 16 (stack alignment). + wasm_stack_size: usize = DEFAULT_WASM_STACK_SIZE, }; /// Errors that can occur during linking @@ -62,20 +120,23 @@ pub const LinkError = error{ OutOfMemory, InvalidArguments, LLVMNotAvailable, -}; + WindowsSDKNotFound, +} || std.zig.system.DetectError; -/// Link object files into an executable using LLD -pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void { - // Check if LLVM is available at compile time - if (comptime !llvm_available) { - return LinkError.LLVMNotAvailable; - } - - var args = std.ArrayList([]const u8).init(allocator); - defer args.deinit(); +/// Build the linker command arguments for the given configuration. +/// Returns the args array that would be passed to LLD. +/// This is used both by link() and formatLinkCommand(). +fn buildLinkArgs(ctx: *CliContext, config: LinkConfig) LinkError!std.array_list.Managed([]const u8) { + // Use arena allocator for all temporary allocations + // Pre-allocate capacity to avoid reallocations (typical command has 20-40 args) + var args = std.array_list.Managed([]const u8).initCapacity(ctx.arena, 64) catch return LinkError.OutOfMemory; // Add platform-specific linker name and arguments - switch (builtin.target.os.tag) { + // Use target OS if provided, otherwise fall back to host OS + const target_os = config.target_os orelse builtin.target.os.tag; + const target_arch = config.target_arch orelse builtin.target.cpu.arch; + + switch (target_os) { .macos => { // Add linker name for macOS try args.append("ld64.lld"); @@ -89,7 +150,7 @@ pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void { // Add architecture flag try args.append("-arch"); - switch (builtin.target.cpu.arch) { + switch (target_arch) { .aarch64 => try args.append("arm64"), .x86_64 => try args.append("x86_64"), else => try args.append("arm64"), // default to arm64 @@ -104,6 +165,14 @@ pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void { // Add SDK path try args.append("-syslibroot"); try args.append("/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"); + + // Link against system libraries on macOS + try args.append("-lSystem"); + + // Link C++ standard library if Tracy is enabled + if (build_options.enable_tracy) { + try args.append("-lc++"); + } }, .linux => { // Add linker name for Linux @@ -113,25 +182,99 @@ pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void { try args.append("-o"); try args.append(config.output_path); - // Suppress LLD warnings + // Prevent hidden linker behaviour -- only explicit platfor mdependencies + try args.append("-nostdlib"); + // Remove unused sections to reduce binary size + try args.append("--gc-sections"); + // TODO make the confirugable instead of using comments + // Suppress linker warnings try args.append("-w"); + // Verbose linker for debugging (uncomment as needed) + // try args.append("--verbose"); + // try args.append("--print-map"); + // try args.append("--error-limit=0"); - // Use static linking to avoid dynamic linker dependency issues - try args.append("-static"); + // Determine target ABI + const target_abi = config.target_abi orelse if (builtin.target.abi == .musl) TargetAbi.musl else TargetAbi.gnu; + + switch (target_abi) { + .musl => { + // Static musl linking + try args.append("-static"); + }, + .gnu => { + // Dynamic GNU linking - dynamic linker path is handled by caller + // for cross-compilation. Only detect locally for native builds + if (config.extra_args.len == 0) { + // Native build - try to detect dynamic linker + if (libc_finder.findLibc(ctx)) |libc_info| { + // We need to copy the path since args holds references + try args.append("-dynamic-linker"); + try args.append(libc_info.dynamic_linker); + } else |err| { + // Fallback to hardcoded path based on architecture + std.log.warn("Failed to detect libc: {}, using fallback", .{err}); + try args.append("-dynamic-linker"); + const fallback_ld = switch (builtin.target.cpu.arch) { + .x86_64 => "/lib64/ld-linux-x86-64.so.2", + .aarch64 => "/lib/ld-linux-aarch64.so.1", + .x86 => "/lib/ld-linux.so.2", + else => "/lib/ld-linux.so.2", + }; + try args.append(fallback_ld); + } + } + // Otherwise, dynamic linker is set via extra_args from caller + }, + } + + // Link C++ standard library if Tracy is enabled + if (build_options.enable_tracy) { + try args.append("-lstdc++"); + } }, .windows => { // Add linker name for Windows COFF try args.append("lld-link"); + const query = std.Target.Query{ + .cpu_arch = target_arch, + .os_tag = .windows, + .abi = .msvc, + .ofmt = .coff, + }; + + const target = try std.zig.system.resolveTargetQuery(query); + + const native_libc = std.zig.LibCInstallation.findNative(.{ + .allocator = ctx.arena, + .target = &target, + }) catch return error.WindowsSDKNotFound; + + if (native_libc.crt_dir) |lib_dir| { + const lib_arg = try std.fmt.allocPrint(ctx.arena, "/libpath:{s}", .{lib_dir}); + try args.append(lib_arg); + } else return error.WindowsSDKNotFound; + + if (native_libc.msvc_lib_dir) |lib_dir| { + const lib_arg = try std.fmt.allocPrint(ctx.arena, "/libpath:{s}", .{lib_dir}); + try args.append(lib_arg); + } else return error.WindowsSDKNotFound; + + if (native_libc.kernel32_lib_dir) |lib_dir| { + const lib_arg = try std.fmt.allocPrint(ctx.arena, "/libpath:{s}", .{lib_dir}); + try args.append(lib_arg); + } else return error.WindowsSDKNotFound; + // Add output argument using Windows style - const out_arg = try std.fmt.allocPrint(allocator, "/out:{s}", .{config.output_path}); + const out_arg = try std.fmt.allocPrint(ctx.arena, "/out:{s}", .{config.output_path}); try args.append(out_arg); // Add subsystem flag (console by default) try args.append("/subsystem:console"); // Add machine type based on target architecture - switch (builtin.target.cpu.arch) { + switch (target_arch) { .x86_64 => try args.append("/machine:x64"), .x86 => try args.append("/machine:x86"), .aarch64 => try args.append("/machine:arm64"), @@ -146,6 +289,43 @@ pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void { // Suppress warnings using Windows style try args.append("/ignore:4217"); // Ignore locally defined symbol imported warnings try args.append("/ignore:4049"); // Ignore locally defined symbol imported warnings + + // Link C++ standard library if Tracy is enabled + if (build_options.enable_tracy) { + try args.append("/defaultlib:msvcprt"); + } + }, + .freestanding => { + // WebAssembly linker (wasm-ld) for freestanding wasm32 target + try args.append("wasm-ld"); + + // Add output argument + try args.append("-o"); + try args.append(config.output_path); + + // Don't look for _start or _main entry point - we export specific functions + try args.append("--no-entry"); + + // Export all symbols (the Roc app exports its entrypoints) + try args.append("--export-all"); + + // Disable garbage collection to preserve host-defined exports (init, handleEvent, update) + // Without this, wasm-ld removes symbols that aren't referenced by the Roc app + try args.append("--no-gc-sections"); + + // Allow undefined symbols (imports from host environment) + try args.append("--allow-undefined"); + + // Set initial memory size (configurable, default 64MB) + // Must be a multiple of 64KB (WASM page size) + const initial_memory_str = std.fmt.allocPrint(ctx.arena, "--initial-memory={d}", .{config.wasm_initial_memory}) catch return LinkError.OutOfMemory; + try args.append(initial_memory_str); + + // Set stack size (configurable, default 8MB) + // Must be a multiple of 16 (stack alignment) + const stack_size_str = std.fmt.allocPrint(ctx.arena, "stack-size={d}", .{config.wasm_stack_size}) catch return LinkError.OutOfMemory; + try args.append("-z"); + try args.append(stack_size_str); }, else => { // Generic ELF linker @@ -160,31 +340,88 @@ pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void { }, } - // Add object files + // For WASM targets, wrap platform files in --whole-archive to include all symbols + // This ensures host exports (init, handleEvent, update) aren't stripped even when + // not referenced by other code + const is_wasm = config.target_format == .wasm; + const is_macos = target_os == .macos; + if (is_wasm and config.platform_files_pre.len > 0) { + try args.append("--whole-archive"); + } + + // Add platform-provided files that come before object files + // Use --whole-archive (or -all_load on macOS) to include all members from static libraries + // This ensures host-exported functions like init, handleEvent, update are included + // even though they're not referenced by the Roc app's compiled code + if (config.platform_files_pre.len > 0) { + if (is_macos) { + // macOS uses -all_load to include all members from static libraries + try args.append("-all_load"); + } else { + try args.append("--whole-archive"); + } + for (config.platform_files_pre) |platform_file| { + try args.append(platform_file); + } + if (!is_macos) { + try args.append("--no-whole-archive"); + } + } + + // Add object files (Roc shim libraries - don't need --whole-archive) for (config.object_files) |obj_file| { try args.append(obj_file); } + // Add platform-provided files that come after object files + // Also use --whole-archive in case there are static libs here too + if (config.platform_files_post.len > 0) { + if (is_macos) { + try args.append("-all_load"); + } else { + try args.append("--whole-archive"); + } + for (config.platform_files_post) |platform_file| { + try args.append(platform_file); + } + if (!is_macos) { + try args.append("--no-whole-archive"); + } + } + // Add any extra arguments for (config.extra_args) |extra_arg| { try args.append(extra_arg); } + return args; +} + +/// Link object files into an executable using LLD +pub fn link(ctx: *CliContext, config: LinkConfig) LinkError!void { + // Check if LLVM is available at compile time + if (comptime !llvm_available) { + return LinkError.LLVMNotAvailable; + } + + const args = try buildLinkArgs(ctx, config); + + // Debug: Print the linker command + std.log.debug("Linker command:", .{}); + for (args.items) |arg| { + std.log.debug(" {s}", .{arg}); + } + // Convert to null-terminated strings for C API - var c_args = allocator.alloc([*:0]const u8, args.items.len) catch return LinkError.OutOfMemory; - defer allocator.free(c_args); + // Arena allocator will clean up all these temporary allocations + var c_args = ctx.arena.alloc([*:0]const u8, args.items.len) catch return LinkError.OutOfMemory; for (args.items, 0..) |arg, i| { - c_args[i] = (allocator.dupeZ(u8, arg) catch return LinkError.OutOfMemory).ptr; - } - defer { - for (c_args) |c_arg| { - allocator.free(std.mem.span(c_arg)); - } + c_args[i] = (ctx.arena.dupeZ(u8, arg) catch return LinkError.OutOfMemory).ptr; } // Call appropriate LLD function based on target format - const success = if (comptime llvm_available) switch (config.target_format) { + const success = switch (config.target_format) { .elf => llvm_externs.ZigLLDLinkELF( @intCast(c_args.len), c_args.ptr, @@ -209,15 +446,47 @@ pub fn link(allocator: Allocator, config: LinkConfig) LinkError!void { config.can_exit_early, config.disable_output, ), - } else false; + }; if (!success) { return LinkError.LinkFailed; } } +/// Format link configuration as a shell command string for manual reproduction. +/// Useful for debugging linking issues by allowing users to run the linker manually. +pub fn formatLinkCommand(ctx: *CliContext, config: LinkConfig) LinkError![]const u8 { + const args = try buildLinkArgs(ctx, config); + + // Join args with spaces, quoting paths that contain spaces or special chars + var result = std.array_list.Managed(u8).init(ctx.arena); + + for (args.items, 0..) |arg, i| { + if (i > 0) result.append(' ') catch return LinkError.OutOfMemory; + + // Quote if contains spaces or shell metacharacters + const needs_quoting = std.mem.indexOfAny(u8, arg, " \t'\"\\$`") != null; + if (needs_quoting) { + result.append('\'') catch return LinkError.OutOfMemory; + // Escape single quotes within the string + for (arg) |c| { + if (c == '\'') { + result.appendSlice("'\\''") catch return LinkError.OutOfMemory; + } else { + result.append(c) catch return LinkError.OutOfMemory; + } + } + result.append('\'') catch return LinkError.OutOfMemory; + } else { + result.appendSlice(arg) catch return LinkError.OutOfMemory; + } + } + + return result.toOwnedSlice() catch return LinkError.OutOfMemory; +} + /// Convenience function to link two object files into an executable -pub fn linkTwoObjects(allocator: Allocator, obj1: []const u8, obj2: []const u8, output: []const u8) LinkError!void { +pub fn linkTwoObjects(ctx: *CliContext, obj1: []const u8, obj2: []const u8, output: []const u8) LinkError!void { if (comptime !llvm_available) { return LinkError.LLVMNotAvailable; } @@ -227,11 +496,11 @@ pub fn linkTwoObjects(allocator: Allocator, obj1: []const u8, obj2: []const u8, .object_files = &.{ obj1, obj2 }, }; - return link(allocator, config); + return link(ctx, config); } /// Convenience function to link multiple object files into an executable -pub fn linkObjects(allocator: Allocator, object_files: []const []const u8, output: []const u8) LinkError!void { +pub fn linkObjects(ctx: *CliContext, object_files: []const []const u8, output: []const u8) LinkError!void { if (comptime !llvm_available) { return LinkError.LLVMNotAvailable; } @@ -241,7 +510,7 @@ pub fn linkObjects(allocator: Allocator, object_files: []const []const u8, outpu .object_files = object_files, }; - return link(allocator, config); + return link(ctx, config); } test "link config creation" { @@ -253,6 +522,8 @@ test "link config creation" { try std.testing.expect(config.target_format == TargetFormat.detectFromSystem()); try std.testing.expectEqualStrings("test_output", config.output_path); try std.testing.expectEqual(@as(usize, 2), config.object_files.len); + try std.testing.expectEqual(@as(usize, 0), config.platform_files_pre.len); + try std.testing.expectEqual(@as(usize, 0), config.platform_files_post.len); } test "target format detection" { @@ -266,12 +537,17 @@ test "target format detection" { test "link error when LLVM not available" { if (comptime !llvm_available) { + var io = Io.init(); + var ctx = CliContext.init(std.testing.allocator, std.testing.allocator, &io, .build); + ctx.initIo(); + defer ctx.deinit(); + const config = LinkConfig{ .output_path = "test_output", .object_files = &.{ "file1.o", "file2.o" }, }; - const result = link(std.testing.allocator, config); + const result = link(&ctx, config); try std.testing.expectError(LinkError.LLVMNotAvailable, result); } } diff --git a/src/cli/main.zig b/src/cli/main.zig index d600b8486f..c0b917b096 100644 --- a/src/cli/main.zig +++ b/src/cli/main.zig @@ -1,10 +1,38 @@ //! Roc command line interface for the new compiler. Entrypoint of the Roc binary. -//! Build with `zig build -Dllvm -Dfuzz -Dsystem-afl=false`. +//! Build with `zig build -Dfuzz -Dsystem-afl=false`. //! Result is at `./zig-out/bin/roc` +//! +//! ## Module Data Modes +//! +//! The CLI supports two modes for passing compiled Roc modules to the interpreter: +//! +//! ### IPC Mode (`roc path/to/app.roc`) +//! - Compiles Roc source to ModuleEnv in shared memory +//! - Spawns interpreter host as child process that maps the shared memory +//! - Fast startup, same-architecture only +//! - See: `setupSharedMemoryWithModuleEnv`, `rocRun` +//! +//! ### Embedded Mode (`roc build path/to/app.roc`) +//! - Serializes ModuleEnv to portable binary format +//! - Embeds serialized data directly into output binary +//! - Cross-architecture support, standalone executables +//! - See: `compileAndSerializeModulesForEmbedding`, `rocBuild` +//! +//! For detailed documentation, see `src/interpreter_shim/README.md`. const std = @import("std"); + +/// Configure std library logging to suppress debug messages in production. +/// This prevents debug logs from polluting stderr which should only contain +/// actual program output (like Stderr.line! calls). +pub const std_options: std.Options = .{ + .log_level = .warn, +}; const build_options = @import("build_options"); const builtin = @import("builtin"); + +// Compile-time flag for module tracing - enabled via `zig build -Dtrace-modules` +const trace_modules = if (@hasDecl(build_options, "trace_modules")) build_options.trace_modules else false; const base = @import("base"); const collections = @import("collections"); const reporting = @import("reporting"); @@ -19,12 +47,42 @@ const unbundle = @import("unbundle"); const ipc = @import("ipc"); const fmt = @import("fmt"); const eval = @import("eval"); -const layout = @import("layout"); const builtins = @import("builtins"); +const lsp = @import("lsp"); +const compiled_builtins = @import("compiled_builtins"); +const builtin_loading = eval.builtin_loading; +const BuiltinTypes = eval.BuiltinTypes; const cli_args = @import("cli_args.zig"); +const roc_target = @import("target.zig"); +pub const targets_validator = @import("targets_validator.zig"); +const platform_validation = @import("platform_validation.zig"); +const cli_context = @import("CliContext.zig"); +const cli_problem = @import("CliProblem.zig"); + +const CliProblem = cli_problem.CliProblem; +const CliContext = cli_context.CliContext; +const Io = cli_context.Io; +const Command = cli_context.Command; +const CliError = cli_context.CliError; +const renderProblem = cli_context.renderProblem; + +comptime { + if (builtin.is_test) { + std.testing.refAllDecls(cli_args); + std.testing.refAllDecls(targets_validator); + std.testing.refAllDecls(platform_validation); + std.testing.refAllDecls(cli_context); + std.testing.refAllDecls(cli_problem); + } +} const bench = @import("bench.zig"); const linker = @import("linker.zig"); +const platform_host_shim = @import("platform_host_shim.zig"); +const builder = @import("builder.zig"); + +/// Check if LLVM is available +const llvm_available = builder.isLLVMAvailable(); const Can = can.Can; const Check = check.Check; @@ -36,8 +94,7 @@ const TimingInfo = compile.package.TimingInfo; const CacheManager = compile.CacheManager; const CacheConfig = compile.CacheConfig; const tokenize = parse.tokenize; -const Interpreter = eval.Interpreter; -const LayoutStore = layout.Store; +const TestRunner = eval.TestRunner; const RocOps = builtins.host_abi.RocOps; const RocAlloc = builtins.host_abi.RocAlloc; const RocDealloc = builtins.host_abi.RocDealloc; @@ -45,11 +102,57 @@ const RocRealloc = builtins.host_abi.RocRealloc; const RocDbg = builtins.host_abi.RocDbg; const RocExpectFailed = builtins.host_abi.RocExpectFailed; const RocCrashed = builtins.host_abi.RocCrashed; +const TestOpsEnv = eval.TestOpsEnv; +const Allocators = base.Allocators; +const CompactWriter = collections.CompactWriter; -const roc_shim_lib = if (builtin.is_test) &[_]u8{} else if (builtin.target.os.tag == .windows) @embedFile("roc_shim.lib") else @embedFile("libroc_shim.a"); +// Import serialization types from the shared module +const SERIALIZED_FORMAT_MAGIC = collections.SERIALIZED_FORMAT_MAGIC; +const SerializedHeader = collections.SerializedHeader; +const SerializedModuleInfo = collections.SerializedModuleInfo; -test { - _ = @import("test_bundle_logic.zig"); +/// Embedded interpreter shim libraries for different targets. +/// The native shim is used for roc run and native builds. +/// Cross-compilation shims are used for roc build --target=. +const ShimLibraries = struct { + /// Native shim (for host platform builds and roc run) + const native = if (builtin.is_test) + &[_]u8{} + else if (builtin.target.os.tag == .windows) + @embedFile("roc_interpreter_shim.lib") + else + @embedFile("libroc_interpreter_shim.a"); + + /// Cross-compilation target shims (Linux musl targets) + const x64musl = if (builtin.is_test) &[_]u8{} else @embedFile("targets/x64musl/libroc_interpreter_shim.a"); + const arm64musl = if (builtin.is_test) &[_]u8{} else @embedFile("targets/arm64musl/libroc_interpreter_shim.a"); + + /// Cross-compilation target shims (Linux glibc targets) + const x64glibc = if (builtin.is_test) &[_]u8{} else @embedFile("targets/x64glibc/libroc_interpreter_shim.a"); + const arm64glibc = if (builtin.is_test) &[_]u8{} else @embedFile("targets/arm64glibc/libroc_interpreter_shim.a"); + + /// WebAssembly target shim (wasm32-freestanding) + const wasm32 = if (builtin.is_test) &[_]u8{} else @embedFile("targets/wasm32/libroc_interpreter_shim.a"); + + /// Get the appropriate shim library bytes for the given target + pub fn forTarget(target: roc_target.RocTarget) []const u8 { + return switch (target) { + .x64musl => x64musl, + .arm64musl => arm64musl, + .x64glibc => x64glibc, + .arm64glibc => arm64glibc, + .wasm32 => wasm32, + // Native/host targets use the native shim + .x64mac, .arm64mac, .x64win, .arm64win => native, + // Fallback for other targets (will use native, may not work for cross-compilation) + else => native, + }; + } +}; + +test "main cli tests" { + _ = @import("libc_finder.zig"); + _ = @import("test_shared_memory_system.zig"); } // Workaround for Zig standard library compilation issue on macOS ARM64. @@ -93,18 +196,41 @@ pub const c = struct { // Platform-specific shared memory implementation const is_windows = builtin.target.os.tag == .windows; +var windows_console_configured = false; +var windows_console_previous_code_page: ?std.os.windows.UINT = null; + +fn ensureWindowsConsoleSupportsAnsiAndUtf8() void { + if (!is_windows) return; + if (windows_console_configured) return; + windows_console_configured = true; + + // Ensure the legacy console interprets escape sequences and UTF-8 output. + const kernel32 = std.os.windows.kernel32; + const current_code_page = kernel32.GetConsoleOutputCP(); + if (current_code_page != 0 and current_code_page != 65001) { + windows_console_previous_code_page = current_code_page; + _ = kernel32.SetConsoleOutputCP(65001); + } + // Note: ANSI escape support is enabled in Io.init() +} + +fn restoreWindowsConsoleCodePage() void { + if (!is_windows) return; + if (windows_console_previous_code_page) |code_page| { + windows_console_previous_code_page = null; + _ = std.os.windows.kernel32.SetConsoleOutputCP(code_page); + } +} + // POSIX shared memory functions const posix = if (!is_windows) struct { extern "c" fn shm_open(name: [*:0]const u8, oflag: c_int, mode: std.c.mode_t) c_int; extern "c" fn shm_unlink(name: [*:0]const u8) c_int; - extern "c" fn mmap(addr: ?*anyopaque, len: usize, prot: c_int, flags: c_int, fd: c_int, offset: std.c.off_t) ?*anyopaque; + extern "c" fn mmap(addr: ?*anyopaque, len: usize, prot: c_int, flags: c_int, fd: c_int, offset: std.c.off_t) *anyopaque; extern "c" fn munmap(addr: *anyopaque, len: usize) c_int; - extern "c" fn fcntl(fd: c_int, cmd: c_int, arg: c_int) c_int; - // fcntl constants - const F_GETFD = 1; - const F_SETFD = 2; - const FD_CLOEXEC = 1; + // MAP_FAILED is (void*)-1, not NULL + const MAP_FAILED: *anyopaque = @ptrFromInt(@as(usize, @bitCast(@as(isize, -1)))); } else struct {}; // Windows shared memory functions @@ -156,6 +282,7 @@ const windows = if (is_windows) struct { lpProcessInformation: *PROCESS_INFORMATION, ) BOOL; extern "kernel32" fn WaitForSingleObject(hHandle: HANDLE, dwMilliseconds: DWORD) DWORD; + extern "kernel32" fn GetExitCodeProcess(hProcess: HANDLE, lpExitCode: *DWORD) BOOL; const HANDLE_FLAG_INHERIT = 0x00000001; const INFINITE = 0xFFFFFFFF; @@ -166,9 +293,83 @@ const benchParse = bench.benchParse; const Allocator = std.mem.Allocator; const ColorPalette = reporting.ColorPalette; +const ReportBuilder = check.ReportBuilder; const legalDetailsFileContent = @embedFile("legal_details"); +/// Render type checking problems as diagnostic reports to stderr. +/// Returns the count of errors (fatal/runtime_error severity). +/// This is shared between rocCheck and rocRun to ensure consistent error reporting. +fn renderTypeProblems( + ctx: *CliContext, + checker: *Check, + module_env: *ModuleEnv, + filename: []const u8, +) usize { + const stderr = ctx.io.stderr(); + + var rb = ReportBuilder.init( + ctx.gpa, + module_env, + module_env, + &checker.snapshots, + filename, + &.{}, + &checker.import_mapping, + ); + defer rb.deinit(); + + var error_count: usize = 0; + var warning_count: usize = 0; + + // Render canonicalization diagnostics (unused variables, etc.) + // Note: getDiagnostics allocates with module_env.gpa, so we must free with that allocator + const diags = module_env.getDiagnostics() catch &.{}; + defer module_env.gpa.free(diags); + for (diags) |d| { + var report = module_env.diagnosticToReport(d, module_env.gpa, filename) catch continue; + defer report.deinit(); + + reporting.renderReportToTerminal(&report, stderr, ColorPalette.ANSI, reporting.ReportingConfig.initColorTerminal()) catch continue; + + if (report.severity == .fatal or report.severity == .runtime_error) { + error_count += 1; + } else if (report.severity == .warning) { + warning_count += 1; + } + } + + // Render type checking problems + for (checker.problems.problems.items) |prob| { + var report = rb.build(prob) catch continue; + defer report.deinit(); + + // Render the diagnostic report to stderr + reporting.renderReportToTerminal(&report, stderr, ColorPalette.ANSI, reporting.ReportingConfig.initColorTerminal()) catch continue; + + if (report.severity == .fatal or report.severity == .runtime_error) { + error_count += 1; + } else if (report.severity == .warning) { + warning_count += 1; + } + } + + // Print summary if there were any problems + if (error_count > 0 or warning_count > 0) { + stderr.writeAll("\n") catch {}; + stderr.print("Found {} error(s) and {} warning(s) for {s}.\n", .{ + error_count, + warning_count, + filename, + }) catch {}; + } + + // Flush stderr to ensure all error output is visible + ctx.io.flush(); + + return error_count; +} + /// Size for shared memory allocator (just virtual address space to reserve) /// /// We pick a large number because we can't resize this without messing up the @@ -180,13 +381,11 @@ else 256 * 1024 * 1024; // 256MB for 32-bit targets /// Cross-platform hardlink creation -fn createHardlink(allocator: Allocator, source: []const u8, dest: []const u8) !void { +fn createHardlink(ctx: *CliContext, source: []const u8, dest: []const u8) !void { if (comptime builtin.target.os.tag == .windows) { // On Windows, use CreateHardLinkW - const source_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, source); - defer allocator.free(source_w); - const dest_w = try std.unicode.utf8ToUtf16LeAllocZ(allocator, dest); - defer allocator.free(dest_w); + const source_w = try std.unicode.utf8ToUtf16LeAllocZ(ctx.arena, source); + const dest_w = try std.unicode.utf8ToUtf16LeAllocZ(ctx.arena, dest); // Declare CreateHardLinkW since it's not in all versions of std const kernel32 = struct { @@ -194,7 +393,7 @@ fn createHardlink(allocator: Allocator, source: []const u8, dest: []const u8) !v lpFileName: [*:0]const u16, lpExistingFileName: [*:0]const u16, lpSecurityAttributes: ?*anyopaque, - ) callconv(std.os.windows.WINAPI) std.os.windows.BOOL; + ) callconv(.winapi) std.os.windows.BOOL; }; if (kernel32.CreateHardLinkW(dest_w, source_w, null) == 0) { @@ -206,10 +405,8 @@ fn createHardlink(allocator: Allocator, source: []const u8, dest: []const u8) !v } } else { // On POSIX systems, use the link system call - const source_c = try allocator.dupeZ(u8, source); - defer allocator.free(source_c); - const dest_c = try allocator.dupeZ(u8, dest); - defer allocator.free(dest_c); + const source_c = try ctx.arena.dupeZ(u8, source); + const dest_c = try ctx.arena.dupeZ(u8, dest); const result = c.link(source_c, dest_c); if (result != 0) { @@ -223,12 +420,12 @@ fn createHardlink(allocator: Allocator, source: []const u8, dest: []const u8) !v } /// Generate a cryptographically secure random ASCII string for directory names -fn generateRandomSuffix(allocator: Allocator) ![]u8 { +fn generateRandomSuffix(ctx: *CliContext) ![]u8 { // TODO: Consider switching to a library like https://github.com/abhinav/temp.zig // for more robust temporary file/directory handling const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - const suffix = try allocator.alloc(u8, 32); + const suffix = try ctx.arena.alloc(u8, 32); // Fill with cryptographically secure random bytes std.crypto.random.bytes(suffix); @@ -241,53 +438,96 @@ fn generateRandomSuffix(allocator: Allocator) ![]u8 { return suffix; } -/// Create the temporary directory structure for fd communication. -/// Returns the path to the executable in the temp directory (caller must free). -/// If a cache directory is provided, it will be used for temporary files; otherwise -/// falls back to the system temp directory. -pub fn createTempDirStructure(allocator: Allocator, exe_path: []const u8, shm_handle: SharedMemoryHandle, cache_dir: ?[]const u8) ![]const u8 { - // Use provided cache dir or fall back to system temp directory - const temp_dir = if (cache_dir) |dir| - try allocator.dupe(u8, dir) - else if (comptime is_windows) - std.process.getEnvVarOwned(allocator, "TEMP") catch - std.process.getEnvVarOwned(allocator, "TMP") catch try allocator.dupe(u8, "C:\\Windows\\Temp") - else - std.process.getEnvVarOwned(allocator, "TMPDIR") catch try allocator.dupe(u8, "/tmp"); - defer allocator.free(temp_dir); +/// Create a unique temporary directory under roc/{version}/{random}/. +/// Returns the path to the directory (allocated from arena, no need to free). +/// Uses system temp directory to avoid race conditions when cache is cleared. +pub fn createUniqueTempDir(ctx: *CliContext) ![]const u8 { + // Get the version-specific temp directory: {temp}/roc/{version} + const version_temp_dir = try CacheConfig.getVersionTempDir(ctx.arena); - // Try up to 10 times to create a unique directory + // Ensure the roc/{version} directory exists + // makePath automatically handles PathAlreadyExists internally + try std.fs.cwd().makePath(version_temp_dir); + + // Try to create a unique subdirectory with random suffix var attempt: u8 = 0; - while (attempt < 10) : (attempt += 1) { - const random_suffix = try generateRandomSuffix(allocator); - errdefer allocator.free(random_suffix); + while (attempt < 6) : (attempt += 1) { + const random_suffix = try generateRandomSuffix(ctx); + const dir_path = try std.fs.path.join(ctx.arena, &.{ version_temp_dir, random_suffix }); - // Create the full path with .txt suffix first - const normalized_temp_dir = if (comptime is_windows) - std.mem.trimRight(u8, temp_dir, "/\\") - else - std.mem.trimRight(u8, temp_dir, "/"); - const dir_name_with_txt = if (comptime is_windows) - try std.fmt.allocPrint(allocator, "{s}\\roc-tmp-{s}.txt", .{ normalized_temp_dir, random_suffix }) - else - try std.fmt.allocPrint(allocator, "{s}/roc-tmp-{s}.txt", .{ normalized_temp_dir, random_suffix }); - errdefer allocator.free(dir_name_with_txt); + // Try to create the directory + std.fs.cwd().makeDir(dir_path) catch |err| switch (err) { + error.PathAlreadyExists => { + // Directory already exists, try again with a new random suffix + continue; + }, + else => { + return err; + }, + }; - // Get the directory path by slicing off the .txt suffix - const dir_path_len = dir_name_with_txt.len - 4; // Remove ".txt" - const temp_dir_path = dir_name_with_txt[0..dir_path_len]; + return dir_path; + } + + // Failed after 6 attempts + return error.FailedToCreateUniqueTempDir; +} + +/// Write shared memory coordination file (.txt) next to the executable. +/// This is the file that the child process reads to find the shared memory fd. +pub fn writeFdCoordinationFile(ctx: *CliContext, temp_exe_path: []const u8, shm_handle: SharedMemoryHandle) !void { + // The coordination file is at {temp_dir}.txt where temp_dir is the directory containing the exe + const temp_dir = std.fs.path.dirname(temp_exe_path) orelse return error.InvalidPath; + + // Ensure we have no trailing slashes + var dir_path = temp_dir; + while (dir_path.len > 0 and (dir_path[dir_path.len - 1] == '/' or dir_path[dir_path.len - 1] == '\\')) { + dir_path = dir_path[0 .. dir_path.len - 1]; + } + + const fd_file_path = try std.fmt.allocPrint(ctx.arena, "{s}.txt", .{dir_path}); + + // Create the file (exclusive - fail if exists to detect collisions) + const fd_file = std.fs.cwd().createFile(fd_file_path, .{ .exclusive = true }) catch |err| { + // Error is handled by caller with ctx.fail() + return err; + }; + defer fd_file.close(); + + // Write shared memory info to file + const fd_str = try std.fmt.allocPrint(ctx.arena, "{}\n{}", .{ shm_handle.fd, shm_handle.size }); + try fd_file.writeAll(fd_str); + try fd_file.sync(); +} + +/// Create the temporary directory structure for fd communication. +/// Returns the path to the executable in the temp directory (allocated from arena, no need to free). +/// Uses the standard roc/{version}/{random}/ structure in the system temp directory. +/// The exe_display_name is the name that will appear in `ps` output (e.g., "app.roc"). +pub fn createTempDirStructure(allocs: *Allocators, exe_path: []const u8, exe_display_name: []const u8, shm_handle: SharedMemoryHandle, _: ?[]const u8) ![]const u8 { + // Get the version-specific temp directory: {temp}/roc/{version} + const version_temp_dir = try CacheConfig.getVersionTempDir(allocs.arena); + + // Ensure the roc/{version} directory exists + // makePath automatically handles PathAlreadyExists internally + try std.fs.cwd().makePath(version_temp_dir); + + // Try to create a unique subdirectory with random suffix + var attempt: u8 = 0; + while (attempt < 6) : (attempt += 1) { + const random_suffix = try generateRandomSuffix(allocs); + const temp_dir_path = try std.fs.path.join(allocs.arena, &.{ version_temp_dir, random_suffix }); + + // The coordination file path is the directory path with .txt appended + const dir_name_with_txt = try std.fmt.allocPrint(allocs.arena, "{s}.txt", .{temp_dir_path}); // Try to create the directory std.fs.cwd().makeDir(temp_dir_path) catch |err| switch (err) { error.PathAlreadyExists => { // Directory already exists, try again with a new random suffix - allocator.free(random_suffix); - allocator.free(dir_name_with_txt); continue; }, else => { - allocator.free(random_suffix); - allocator.free(dir_name_with_txt); return err; }, }; @@ -297,23 +537,18 @@ pub fn createTempDirStructure(allocator: Allocator, exe_path: []const u8, shm_ha error.PathAlreadyExists => { // File already exists, remove the directory and try again std.fs.cwd().deleteDir(temp_dir_path) catch {}; - allocator.free(random_suffix); - allocator.free(dir_name_with_txt); continue; }, else => { // Clean up directory on other errors std.fs.cwd().deleteDir(temp_dir_path) catch {}; - allocator.free(random_suffix); - allocator.free(dir_name_with_txt); return err; }, }; // Note: We'll close this explicitly later, before spawning the child // Write shared memory info to file (POSIX only - Windows uses command line args) - const fd_str = try std.fmt.allocPrint(allocator, "{}\n{}", .{ shm_handle.fd, shm_handle.size }); - defer allocator.free(fd_str); + const fd_str = try std.fmt.allocPrint(allocs.arena, "{}\n{}", .{ shm_handle.fd, shm_handle.size }); try fd_file.writeAll(fd_str); @@ -322,90 +557,325 @@ pub fn createTempDirStructure(allocator: Allocator, exe_path: []const u8, shm_ha try fd_file.sync(); // Ensure data is written to disk fd_file.close(); - // Create hardlink to executable in temp directory - const exe_basename = std.fs.path.basename(exe_path); - const temp_exe_path = try std.fs.path.join(allocator, &.{ temp_dir_path, exe_basename }); - defer allocator.free(temp_exe_path); + // Create hardlink to executable in temp directory with display name + const temp_exe_path = try std.fs.path.join(allocs.arena, &.{ temp_dir_path, exe_display_name }); // Try to create a hardlink first (more efficient than copying) - createHardlink(allocator, exe_path, temp_exe_path) catch { + createHardlink(allocs, exe_path, temp_exe_path) catch { // If hardlinking fails for any reason, fall back to copying // Common reasons: cross-device link, permissions, file already exists try std.fs.cwd().copyFile(exe_path, std.fs.cwd(), temp_exe_path, .{}); }; - // Allocate and return just the executable path - const final_exe_path = try allocator.dupe(u8, temp_exe_path); - - // Free all temporary allocations - allocator.free(dir_name_with_txt); - allocator.free(random_suffix); - - return final_exe_path; + return temp_exe_path; } - // Failed after 10 attempts + // Failed after 6 attempts return error.FailedToCreateUniqueTempDir; } +var debug_allocator: std.heap.DebugAllocator(.{}) = .{ + .backing_allocator = std.heap.c_allocator, +}; + /// The CLI entrypoint for the Roc compiler. pub fn main() !void { + // Install stack overflow handler early, before any significant work. + // This gives us a helpful error message instead of a generic segfault + // if the compiler blows the stack (e.g., due to infinite recursion in type translation). + _ = base.stack_overflow.install(); + var gpa_tracy: tracy.TracyAllocator(null) = undefined; - var gpa = std.heap.c_allocator; + var gpa, const is_safe = gpa: { + if (builtin.os.tag == .freestanding) break :gpa .{ std.heap.wasm_allocator, false }; + break :gpa switch (builtin.mode) { + .Debug, .ReleaseSafe => .{ debug_allocator.allocator(), true }, + .ReleaseFast, .ReleaseSmall => .{ std.heap.c_allocator, false }, + }; + }; + defer restoreWindowsConsoleCodePage(); + defer if (is_safe) { + const mem_state = debug_allocator.deinit(); + std.debug.assert(mem_state == .ok); + }; if (tracy.enable_allocation) { gpa_tracy = tracy.tracyAllocator(gpa); gpa = gpa_tracy.allocator(); } - var arena_impl = std.heap.ArenaAllocator.init(gpa); - defer arena_impl.deinit(); - const arena = arena_impl.allocator(); + var allocs: Allocators = undefined; + allocs.initInPlace(gpa); + defer allocs.deinit(); - const args = try std.process.argsAlloc(arena); + const args = try std.process.argsAlloc(allocs.arena); + + mainArgs(&allocs, args) catch { + // Error messages have already been printed by the individual functions. + // Exit cleanly without showing a stack trace to the user. + if (tracy.enable) { + tracy.waitForShutdown() catch {}; + } + restoreWindowsConsoleCodePage(); + std.process.exit(1); + }; - const result = mainArgs(gpa, arena, args); if (tracy.enable) { try tracy.waitForShutdown(); } - return result; } -fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { +fn mainArgs(allocs: *Allocators, args: []const []const u8) !void { const trace = tracy.trace(@src()); defer trace.end(); - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); + ensureWindowsConsoleSupportsAnsiAndUtf8(); - const parsed_args = try cli_args.parse(gpa, args[1..]); - defer parsed_args.deinit(gpa); + // Start background cache cleanup on a separate thread. + // This is a fire-and-forget thread that: + // - Cleans up stale temp directories (>5 min old) + // - Cleans up old persistent cache files (>30 days old) + // - Exits automatically when done + // + // We intentionally don't join the thread. If the main process exits before + // cleanup completes, the OS will automatically terminate the cleanup thread. + // This ensures cleanup never delays compilation or execution. + // + // Uses page_allocator instead of GPA to avoid leak detection false positives + // (the thread may still be running when the main thread's leak check fires). + if (compile.CacheCleanup.startBackgroundCleanup(std.heap.page_allocator)) |_| { + // Thread started successfully, will run in background + } else |_| { + // Non-fatal: cleanup failure shouldn't prevent compilation + std.log.debug("Failed to start background cleanup thread", .{}); + } + + // Create I/O interface - this is passed to all command handlers via ctx + var io = Io.init(); + + const parsed_args = try cli_args.parse(allocs.arena, args[1..]); + + // Determine command for context + const command: Command = switch (parsed_args) { + .run => .run, + .build => .build, + .check => .check, + .test_cmd => .test_cmd, + .fmt => .fmt, + .bundle => .bundle, + .unbundle => .unbundle, + else => .unknown, + }; + + // Create CLI context at the top level - this is passed to all command handlers + var ctx = CliContext.init(allocs.gpa, allocs.arena, &io, command); + ctx.initIo(); // Must be called after ctx is at its final stack location + defer ctx.deinit(); // deinit flushes I/O try switch (parsed_args) { - .run => |run_args| rocRun(gpa, run_args), - .check => |check_args| rocCheck(gpa, check_args), - .build => |build_args| rocBuild(gpa, build_args), - .bundle => |bundle_args| rocBundle(gpa, bundle_args), - .unbundle => |unbundle_args| rocUnbundle(gpa, unbundle_args), - .format => |format_args| rocFormat(gpa, arena, format_args), - .test_cmd => |test_args| rocTest(gpa, test_args), - .repl => rocRepl(gpa), - .version => stdout.print("Roc compiler version {s}\n", .{build_options.compiler_version}), - .docs => |docs_args| rocDocs(gpa, docs_args), - .help => |help_message| stdout.writeAll(help_message), - .licenses => stdout.writeAll(legalDetailsFileContent), + .run => |run_args| { + if (std.mem.eql(u8, run_args.path, "main.roc")) { + std.fs.cwd().access(run_args.path, .{}) catch |err| switch (err) { + error.FileNotFound => { + const cwd_path = std.fs.cwd().realpathAlloc(allocs.arena, ".") catch |real_err| { + ctx.io.stderr().print( + "Error: No app file specified and default 'main.roc' was not found. Additionally, the current directory could not be resolved: {}\n", + .{real_err}, + ) catch {}; + return error.FileNotFound; + }; + ctx.io.stderr().print( + "Error: No app file specified and default 'main.roc' was not found in {s}\n", + .{cwd_path}, + ) catch {}; + ctx.io.stderr().print( + "\nHint: pass an explicit path (e.g. `roc my-app.roc`) or create a 'main.roc' in that directory.\n", + .{}, + ) catch {}; + return error.FileNotFound; + }, + else => { + ctx.io.stderr().print( + "Error: Unable to access default 'main.roc': {}\n", + .{err}, + ) catch {}; + return err; + }, + }; + } + + rocRun(&ctx, run_args) catch |err| switch (err) { + error.CliError => { + // Problems already recorded in context, render them below + }, + else => return err, + }; + }, + .check => |check_args| rocCheck(&ctx, check_args), + .build => |build_args| rocBuild(&ctx, build_args) catch |err| switch (err) { + error.CliError => { + // Problems already recorded in context, render them below + }, + else => return err, + }, + .bundle => |bundle_args| rocBundle(&ctx, bundle_args), + .unbundle => |unbundle_args| rocUnbundle(&ctx, unbundle_args), + .fmt => |format_args| rocFormat(&ctx, format_args), + .test_cmd => |test_args| try rocTest(&ctx, test_args), + .repl => rocRepl(&ctx), + .version => ctx.io.stdout().print("Roc compiler version {s}\n", .{build_options.compiler_version}), + .docs => |docs_args| rocDocs(&ctx, docs_args), + .experimental_lsp => |lsp_args| try lsp.runWithStdIo(allocs.gpa, .{ + .transport = lsp_args.debug_io, + .build = lsp_args.debug_build, + .syntax = lsp_args.debug_syntax, + .server = lsp_args.debug_server, + }), + .help => |help_message| { + try ctx.io.stdout().writeAll(help_message); + }, + .licenses => { + try ctx.io.stdout().writeAll(legalDetailsFileContent); + }, .problem => |problem| { try switch (problem) { - .missing_flag_value => |details| stderr.print("Error: no value was supplied for {s}\n", .{details.flag}), - .unexpected_argument => |details| stderr.print("Error: roc {s} received an unexpected argument: `{s}`\n", .{ details.cmd, details.arg }), - .invalid_flag_value => |details| stderr.print("Error: `{s}` is not a valid value for {s}. The valid options are {s}\n", .{ details.value, details.flag, details.valid_options }), + .missing_flag_value => |details| ctx.io.stderr().print("Error: no value was supplied for {s}\n", .{details.flag}), + .unexpected_argument => |details| ctx.io.stderr().print("Error: roc {s} received an unexpected argument: `{s}`\n", .{ details.cmd, details.arg }), + .invalid_flag_value => |details| ctx.io.stderr().print("Error: `{s}` is not a valid value for {s}. The valid options are {s}\n", .{ details.value, details.flag, details.valid_options }), }; - std.process.exit(1); + return error.InvalidArguments; }, }; + + // Render any problems accumulated during command execution + if (ctx.hasProblems()) { + try ctx.renderProblemsTo(ctx.io.stderr()); + if (ctx.hasErrors()) { + return error.CliError; + } + } } -fn rocRun(gpa: Allocator, args: cli_args.RunArgs) void { +/// Generate platform host shim object file using LLVM. +/// Returns the path to the generated object file (allocated from arena, no need to free), or null if LLVM unavailable. +/// If serialized_module is provided, it will be embedded in the binary (for roc build). +/// If serialized_module is null, the binary will use IPC to get module data (for roc run). +/// If debug is true, include debug information in the generated object file. +fn generatePlatformHostShim(ctx: *CliContext, cache_dir: []const u8, entrypoint_names: []const []const u8, target: builder.RocTarget, serialized_module: ?[]const u8, debug: bool) !?[]const u8 { + // Check if LLVM is available (this is a compile-time check) + if (!llvm_available) { + std.log.debug("LLVM not available, skipping platform host shim generation", .{}); + return null; + } + + const std_zig_llvm = @import("std").zig.llvm; + const Builder = std_zig_llvm.Builder; + + // Create std.Target for the target RocTarget + // This is needed so the LLVM Builder generates correct pointer sizes + const query = std.Target.Query{ + .cpu_arch = target.toCpuArch(), + .os_tag = target.toOsTag(), + }; + const std_target = std.zig.system.resolveTargetQuery(query) catch |err| { + return ctx.fail(.{ .shim_generation_failed = .{ .err = err } }); + }; + + // Create LLVM Builder with the correct target + var llvm_builder = Builder.init(.{ + .allocator = ctx.gpa, + .name = "roc_platform_shim", + .target = &std_target, + .triple = target.toTriple(), + }) catch |err| { + return ctx.fail(.{ .shim_generation_failed = .{ .err = err } }); + }; + defer llvm_builder.deinit(); + + // Create entrypoints array from the provided names + var entrypoints = try std.array_list.Managed(platform_host_shim.EntryPoint).initCapacity(ctx.arena, 8); + + for (entrypoint_names, 0..) |name, idx| { + try entrypoints.append(.{ .name = name, .idx = @intCast(idx) }); + } + + // Create the complete platform shim + // Note: Symbol names include platform-specific prefixes (underscore for macOS) + // serialized_module is null for roc run (IPC mode) or contains data for roc build (embedded mode) + platform_host_shim.createInterpreterShim(&llvm_builder, entrypoints.items, target, serialized_module) catch |err| { + return ctx.fail(.{ .shim_generation_failed = .{ .err = err } }); + }; + + // Generate paths for temporary files + // Use a hash of the serialized module content to avoid race conditions when multiple + // builds run in parallel. Each unique module content gets its own shim files. + const content_hash = if (serialized_module) |module_bytes| + std.hash.Crc32.hash(module_bytes) + else + 0; // For IPC mode (roc run), use a fixed name since there's no embedded data + + const bitcode_filename = std.fmt.allocPrint(ctx.arena, "platform_shim_{x}.bc", .{content_hash}) catch |err| { + return ctx.fail(.{ .shim_generation_failed = .{ .err = err } }); + }; + const object_filename = std.fmt.allocPrint(ctx.arena, "platform_shim_{x}.o", .{content_hash}) catch |err| { + return ctx.fail(.{ .shim_generation_failed = .{ .err = err } }); + }; + + const bitcode_path = std.fs.path.join(ctx.arena, &.{ cache_dir, bitcode_filename }) catch |err| { + return ctx.fail(.{ .shim_generation_failed = .{ .err = err } }); + }; + + const object_path = std.fs.path.join(ctx.arena, &.{ cache_dir, object_filename }) catch |err| { + return ctx.fail(.{ .shim_generation_failed = .{ .err = err } }); + }; + + // Generate bitcode first + const producer = Builder.Producer{ + .name = "Roc Platform Host Shim Generator", + .version = .{ .major = 1, .minor = 0, .patch = 0 }, + }; + + const bitcode = llvm_builder.toBitcode(ctx.gpa, producer) catch |err| { + return ctx.fail(.{ .object_compilation_failed = .{ .path = bitcode_path, .err = err } }); + }; + defer ctx.gpa.free(bitcode); + + // Write bitcode to file + const bc_file = std.fs.cwd().createFile(bitcode_path, .{}) catch |err| { + return ctx.fail(.{ .file_write_failed = .{ .path = bitcode_path, .err = err } }); + }; + defer bc_file.close(); + + // Convert u32 array to bytes for writing + const bytes = std.mem.sliceAsBytes(bitcode); + bc_file.writeAll(bytes) catch |err| { + return ctx.fail(.{ .file_write_failed = .{ .path = bitcode_path, .err = err } }); + }; + + const compile_config = builder.CompileConfig{ + .input_path = bitcode_path, + .output_path = object_path, + .optimization = .speed, + .target = target, + .debug = debug, // Use the debug flag passed from caller + }; + + if (builder.compileBitcodeToObject(ctx.gpa, compile_config)) |success| { + if (!success) { + std.log.warn("LLVM compilation not ready, falling back to clang", .{}); + return error.LLVMCompilationFailed; + } + } else |err| { + std.log.warn("Failed to compile with embedded LLVM: {}, falling back to clang", .{err}); + return error.LLVMCompilationFailed; + } + + std.log.debug("Generated platform host shim: {s}", .{object_path}); + + return object_path; +} + +fn rocRun(ctx: *CliContext, args: cli_args.RunArgs) !void { const trace = tracy.trace(@src()); defer trace.end(); @@ -414,143 +884,309 @@ fn rocRun(gpa: Allocator, args: cli_args.RunArgs) void { .enabled = !args.no_cache, .verbose = false, }; - var cache_manager = CacheManager.init(gpa, cache_config, Filesystem.default()); + var cache_manager = CacheManager.init(ctx.gpa, cache_config, Filesystem.default()); // Create cache directory for linked interpreter executables - const cache_dir = cache_manager.config.getCacheEntriesDir(gpa) catch |err| { - std.log.err("Failed to get cache directory: {}\n", .{err}); - std.process.exit(1); + const exe_cache_dir = cache_manager.config.getExeCacheDir(ctx.arena) catch |err| { + return ctx.fail(.{ .cache_dir_unavailable = .{ .reason = @errorName(err) } }); }; - defer gpa.free(cache_dir); - const exe_cache_dir = std.fs.path.join(gpa, &.{ cache_dir, "executables" }) catch |err| { - std.log.err("Failed to create executable cache path: {}\n", .{err}); - std.process.exit(1); - }; - defer gpa.free(exe_cache_dir); std.fs.cwd().makePath(exe_cache_dir) catch |err| switch (err) { error.PathAlreadyExists => {}, else => { - std.log.err("Failed to create cache directory: {}\n", .{err}); - std.process.exit(1); + return ctx.fail(.{ .directory_create_failed = .{ .path = exe_cache_dir, .err = err } }); }, }; - // Generate executable name based on the roc file path - // TODO use something more interesting like a hash from the platform.main or platform/host.a etc - const exe_name = std.fmt.allocPrint(gpa, "roc_run_{}", .{std.hash.crc.Crc32.hash(args.path)}) catch |err| { - std.log.err("Failed to generate executable name: {}\n", .{err}); - std.process.exit(1); - }; - defer gpa.free(exe_name); + // The final executable name seen in `ps` is the roc filename (e.g., "app.roc") + const exe_display_name = std.fs.path.basename(args.path); - const exe_path = std.fs.path.join(gpa, &.{ exe_cache_dir, exe_name }) catch |err| { - std.log.err("Failed to create executable path: {}\n", .{err}); - std.process.exit(1); - }; - defer gpa.free(exe_path); + // Display name for temp directory (what shows in ps) + const exe_display_name_with_ext = if (builtin.target.os.tag == .windows) + std.fmt.allocPrint(ctx.arena, "{s}.exe", .{exe_display_name}) catch |err| { + return ctx.fail(.{ .cache_dir_unavailable = .{ .reason = @errorName(err) } }); + } + else + ctx.arena.dupe(u8, exe_display_name) catch |err| { + return ctx.fail(.{ .cache_dir_unavailable = .{ .reason = @errorName(err) } }); + }; - // Check if the interpreter executable already exists (cached) - const exe_exists = if (args.no_cache) false else blk: { - std.fs.accessAbsolute(exe_path, .{}) catch { + // Cache executable name uses hash of path (no PID - collision is fine since same content) + const exe_cache_name = std.fmt.allocPrint(ctx.arena, "roc_{x}", .{std.hash.crc.Crc32.hash(args.path)}) catch |err| { + return ctx.fail(.{ .cache_dir_unavailable = .{ .reason = @errorName(err) } }); + }; + + const exe_cache_name_with_ext = if (builtin.target.os.tag == .windows) + std.fmt.allocPrint(ctx.arena, "{s}.exe", .{exe_cache_name}) catch |err| { + return ctx.fail(.{ .cache_dir_unavailable = .{ .reason = @errorName(err) } }); + } + else + ctx.arena.dupe(u8, exe_cache_name) catch |err| { + return ctx.fail(.{ .cache_dir_unavailable = .{ .reason = @errorName(err) } }); + }; + + const exe_cache_path = std.fs.path.join(ctx.arena, &.{ exe_cache_dir, exe_cache_name_with_ext }) catch |err| { + return ctx.fail(.{ .cache_dir_unavailable = .{ .reason = @errorName(err) } }); + }; + + // Create unique temp directory for this build (uses PID for uniqueness) + const temp_dir_path = createUniqueTempDir(ctx) catch |err| { + return ctx.fail(.{ .temp_dir_failed = .{ .err = err } }); + }; + + // The executable is built directly in the temp dir with the display name + const exe_path = std.fs.path.join(ctx.arena, &.{ temp_dir_path, exe_display_name_with_ext }) catch |err| { + return ctx.fail(.{ .cache_dir_unavailable = .{ .reason = @errorName(err) } }); + }; + + // First, parse the app file to get the platform reference + const platform_spec = try extractPlatformSpecFromApp(ctx, args.path); + + // Resolve platform paths from the platform spec (relative to app file directory) + const app_dir = std.fs.path.dirname(args.path) orelse "."; + const platform_paths = try resolvePlatformSpecToPaths(ctx, platform_spec, app_dir); + + // Use native detection for shim generation to match embedded shim library + const shim_target = builder.RocTarget.detectNative(); + + // Validate platform header and get link spec for native target + var link_spec: ?roc_target.TargetLinkSpec = null; + var targets_config: ?roc_target.TargetsConfig = null; + if (platform_paths.platform_source_path) |platform_source| { + if (platform_validation.validatePlatformHeader(ctx.arena, platform_source)) |validation| { + targets_config = validation.config; + + // Check if this is a static_lib-only platform (no exe targets) + if (validation.config.exe.len == 0 and validation.config.static_lib.len > 0) { + ctx.io.stderr().print("Error: This platform only produces static libraries.\n\n", .{}) catch {}; + ctx.io.stderr().print("Static library platforms produce .a/.lib/.wasm files that must be\n", .{}) catch {}; + ctx.io.stderr().print("linked by a host application. Use 'roc build' instead to produce\n", .{}) catch {}; + ctx.io.stderr().print("the library artifact.\n", .{}) catch {}; + return error.UnsupportedTarget; + } + + // Validate that the native target is supported + platform_validation.validateTargetSupported(validation.config, shim_target, .exe) catch |err| { + switch (err) { + error.UnsupportedTarget => { + // Create a nice formatted error report + const result = platform_validation.createUnsupportedTargetResult( + platform_source, + shim_target, + .exe, + validation.config, + ); + _ = platform_validation.renderValidationError(ctx.gpa, result, ctx.io.stderr()); + return error.UnsupportedTarget; + }, + else => {}, + } + }; + + // Get the link spec for native target + link_spec = validation.config.getLinkSpec(shim_target, .exe); + } else |err| { + switch (err) { + error.MissingTargetsSection => { + ctx.io.stderr().print("Error: Platform is missing a targets section.\n\n", .{}) catch {}; + ctx.io.stderr().print("All platforms must have a 'targets:' section in their header\n", .{}) catch {}; + ctx.io.stderr().print("that specifies which targets are supported and what files to link.\n", .{}) catch {}; + return error.PlatformNotSupported; + }, + else => { + std.log.debug("Could not validate platform header: {}", .{err}); + }, + } + } + } + + // All platforms must have a targets section with a link spec for the native target + const validated_link_spec = link_spec orelse { + ctx.io.stderr().print("Error: Platform does not support the native target.\n\n", .{}) catch {}; + ctx.io.stderr().print("The platform's targets section must specify files to link for\n", .{}) catch {}; + ctx.io.stderr().print("the current system. Check the platform header for supported targets.\n", .{}) catch {}; + return error.PlatformNotSupported; + }; + + // Extract entrypoints from platform source file + var entrypoints = std.array_list.Managed([]const u8).initCapacity(ctx.arena, 32) catch { + return error.OutOfMemory; + }; + + if (platform_paths.platform_source_path) |platform_source| { + extractEntrypointsFromPlatform(ctx, platform_source, &entrypoints) catch |err| { + return ctx.fail(.{ .entrypoint_extraction_failed = .{ + .path = platform_source, + .reason = @errorName(err), + } }); + }; + } else { + return ctx.fail(.{ .entrypoint_extraction_failed = .{ + .path = platform_paths.platform_source_path orelse "", + .reason = "No platform source file found for entrypoint extraction", + } }); + } + + // Check if the interpreter executable already exists in cache + const cache_exists = if (args.no_cache) false else blk: { + std.fs.accessAbsolute(exe_cache_path, .{}) catch { break :blk false; }; break :blk true; }; - if (!exe_exists) { - - // Resolve platform from app header - const host_path = resolvePlatformHost(gpa, args.path) catch |err| { - std.log.err("Failed to resolve platform: {}\n", .{err}); - std.process.exit(1); + if (cache_exists) { + // Cached executable exists - hardlink from cache to temp dir + std.log.debug("Using cached executable: {s}", .{exe_cache_path}); + createHardlink(ctx, exe_cache_path, exe_path) catch |err| { + // If hardlinking fails, fall back to copying + std.log.debug("Hardlink from cache failed, copying: {}", .{err}); + std.fs.cwd().copyFile(exe_cache_path, std.fs.cwd(), exe_path, .{}) catch |copy_err| { + return ctx.fail(.{ .file_write_failed = .{ + .path = exe_path, + .err = copy_err, + } }); + }; }; - defer gpa.free(host_path); + } else { - // Check for cached shim library, extract if not present + // Extract shim library to temp dir to avoid race conditions const shim_filename = if (builtin.target.os.tag == .windows) "roc_shim.lib" else "libroc_shim.a"; - const shim_path = std.fs.path.join(gpa, &.{ exe_cache_dir, shim_filename }) catch |err| { - std.log.err("Failed to create shim library path: {}\n", .{err}); - std.process.exit(1); - }; - defer gpa.free(shim_path); - - // Extract shim if not cached or if --no-cache is used - const shim_exists = if (args.no_cache) false else blk: { - std.fs.cwd().access(shim_path, .{}) catch { - break :blk false; - }; - break :blk true; + const shim_path = std.fs.path.join(ctx.arena, &.{ temp_dir_path, shim_filename }) catch { + return error.OutOfMemory; }; - if (!shim_exists) { - // Shim not found in cache or cache disabled, extract it - extractReadRocFilePathShimLibrary(gpa, shim_path) catch |err| { - std.log.err("Failed to extract read roc file path shim library: {}\n", .{err}); - std.process.exit(1); - }; - } + // Always extract to temp dir (unique per process, no race condition) + // For roc run, we always use the native shim (null target) + extractReadRocFilePathShimLibrary(ctx, shim_path, null) catch |err| { + return ctx.fail(.{ .shim_generation_failed = .{ .err = err } }); + }; + + // Generate platform host shim using the detected entrypoints + // Use temp dir to avoid race conditions when multiple processes run in parallel + // Pass null for serialized_module since roc run uses IPC mode + // Auto-enable debug when roc is built in debug mode (no explicit --debug flag for roc run) + const platform_shim_path = try generatePlatformHostShim(ctx, temp_dir_path, entrypoints.items, shim_target, null, builtin.mode == .Debug); // Link the host.a with our shim to create the interpreter executable using our linker // Try LLD first, fallback to clang if LLVM is not available - var extra_args = std.ArrayList([]const u8).init(gpa); - defer extra_args.deinit(); + var extra_args = std.array_list.Managed([]const u8).initCapacity(ctx.arena, 32) catch { + return error.OutOfMemory; + }; // Add system libraries for macOS if (builtin.target.os.tag == .macos) { extra_args.append("-lSystem") catch { - std.log.err("Failed to allocate memory for linker args\n", .{}); - std.process.exit(1); + return error.OutOfMemory; }; } + // Build object files list from the link spec items + // Items are linked in the order specified in the targets section + var object_files = std.array_list.Managed([]const u8).initCapacity(ctx.arena, 16) catch { + return error.OutOfMemory; + }; + + // Get the platform directory for resolving relative paths + const platform_dir = if (platform_paths.platform_source_path) |p| + std.fs.path.dirname(p) orelse "." + else + "."; + + // Get files_dir and target name for path resolution + const files_dir = if (targets_config) |cfg| cfg.files_dir orelse "targets" else "targets"; + const target_name = @tagName(validated_link_spec.target); + + std.log.debug("Platform dir: {s}, files_dir: {s}, target: {s}", .{ platform_dir, files_dir, target_name }); + + // Process each link item in order + for (validated_link_spec.items) |item| { + switch (item) { + .file_path => |file_name| { + // Resolve path: platform_dir / files_dir / target_name / file_name + const full_path = std.fs.path.join(ctx.arena, &.{ + platform_dir, files_dir, target_name, file_name, + }) catch { + return error.OutOfMemory; + }; + std.log.debug("Adding link item: {s}", .{full_path}); + object_files.append(full_path) catch { + return error.OutOfMemory; + }; + }, + .app => { + // Add the compiled Roc application (shim) + std.log.debug("Adding app (shim): {s}", .{shim_path}); + object_files.append(shim_path) catch { + return error.OutOfMemory; + }; + // Also add platform shim if available + if (platform_shim_path) |path| { + object_files.append(path) catch { + return error.OutOfMemory; + }; + } + }, + .win_gui => { + // Windows GUI flag - handled separately in linker config + std.log.debug("win_gui flag detected", .{}); + }, + } + } + + // Determine ABI from target (for musl detection) + const target_abi: ?linker.TargetAbi = if (validated_link_spec.target.isStatic()) .musl else null; + std.log.debug("Target ABI: {?}", .{target_abi}); + + // No pre/post files needed - everything comes from link spec in order + const empty_files: []const []const u8 = &.{}; + const link_config = linker.LinkConfig{ + .target_abi = target_abi, .output_path = exe_path, - .object_files = &.{ host_path, shim_path }, + .object_files = object_files.items, + .platform_files_pre = empty_files, + .platform_files_post = empty_files, .extra_args = extra_args.items, .can_exit_early = false, .disable_output = false, }; - linker.link(gpa, link_config) catch |err| switch (err) { - linker.LinkError.LLVMNotAvailable => { - // Fallback to clang when LLVM is not available - const link_result = std.process.Child.run(.{ - .allocator = gpa, - .argv = &.{ "clang", "-o", exe_path, host_path, shim_path }, - }) catch |clang_err| { - std.log.err("Failed to link executable with both LLD and clang: LLD unavailable, clang error: {}\n", .{clang_err}); - std.process.exit(1); - }; - defer gpa.free(link_result.stdout); - defer gpa.free(link_result.stderr); - if (link_result.term.Exited != 0) { - std.log.err("Linker failed with exit code: {}\n", .{link_result.term.Exited}); - if (link_result.stderr.len > 0) { - std.log.err("Linker stderr: {s}\n", .{link_result.stderr}); - } - if (link_result.stdout.len > 0) { - std.log.err("Linker stdout: {s}\n", .{link_result.stdout}); - } - std.process.exit(1); - } - }, - linker.LinkError.LinkFailed => { - std.log.err("LLD linker failed to create executable\n", .{}); - std.process.exit(1); - }, - else => { - std.log.err("Failed to link executable: {}\n", .{err}); - std.process.exit(1); - }, + linker.link(ctx, link_config) catch |err| { + return ctx.fail(.{ .linker_failed = .{ + .err = err, + .target = @tagName(validated_link_spec.target), + } }); + }; + + // After building, hardlink to cache for future runs + // Force-hardlink (delete existing first) since hash collision means identical content + std.log.debug("Caching executable to: {s}", .{exe_cache_path}); + std.fs.cwd().deleteFile(exe_cache_path) catch |err| switch (err) { + error.FileNotFound => {}, // OK, doesn't exist + else => std.log.debug("Could not delete existing cache file: {}", .{err}), + }; + createHardlink(ctx, exe_path, exe_cache_path) catch |err| { + // If hardlinking fails, fall back to copying + std.log.debug("Hardlink to cache failed, copying: {}", .{err}); + std.fs.cwd().copyFile(exe_path, std.fs.cwd(), exe_cache_path, .{}) catch |copy_err| { + // Non-fatal - just means future runs won't be cached + std.log.debug("Failed to copy to cache: {}", .{copy_err}); + }; }; } // Set up shared memory with ModuleEnv - const shm_handle = setupSharedMemoryWithModuleEnv(gpa, args.path) catch |err| { - std.log.err("Failed to set up shared memory with ModuleEnv: {}\n", .{err}); - std.process.exit(1); - }; + std.log.debug("Setting up shared memory for Roc file: {s}", .{args.path}); + const shm_result = try setupSharedMemoryWithModuleEnv(ctx, args.path, args.allow_errors); + std.log.debug("Shared memory setup complete, size: {} bytes", .{shm_result.handle.size}); + + // Check for errors - abort unless --allow-errors flag is set + if (shm_result.error_count > 0 and !args.allow_errors) { + return error.TypeCheckingFailed; + } + + const shm_handle = shm_result.handle; // Ensure we clean up shared memory resources on all exit paths defer { @@ -563,44 +1199,109 @@ fn rocRun(gpa: Allocator, args: cli_args.RunArgs) void { } } + std.log.debug("Launching interpreter executable: {s}", .{exe_path}); if (comptime is_windows) { // Windows: Use handle inheritance approach - runWithWindowsHandleInheritance(gpa, exe_path, shm_handle) catch |err| { - std.log.err("Failed to run with Windows handle inheritance: {}\n", .{err}); - std.process.exit(1); - }; + std.log.debug("Using Windows handle inheritance approach", .{}); + try runWithWindowsHandleInheritance(ctx, exe_path, shm_handle, args.app_args); } else { // POSIX: Use existing file descriptor inheritance approach - runWithPosixFdInheritance(gpa, exe_path, shm_handle, &cache_manager) catch |err| { - std.log.err("Failed to run with POSIX fd inheritance: {}\n", .{err}); - std.process.exit(1); - }; + std.log.debug("Using POSIX file descriptor inheritance approach", .{}); + try runWithPosixFdInheritance(ctx, exe_path, shm_handle, args.app_args); } + std.log.debug("Interpreter execution completed", .{}); +} + +/// Append an argument to a command line buffer with proper Windows quoting. +/// Windows command line parsing rules: +/// - Arguments containing spaces, tabs, or quotes must be quoted +/// - Embedded quotes must be escaped with backslash: " -> \" +/// - Backslashes before quotes must be doubled: \" -> \\" +fn appendWindowsQuotedArg(cmd_builder: *std.array_list.Managed(u8), arg: []const u8) !void { + const needs_quoting = arg.len == 0 or std.mem.indexOfAny(u8, arg, " \t\"") != null; + + if (!needs_quoting) { + try cmd_builder.appendSlice(arg); + return; + } + + try cmd_builder.append('"'); + var backslash_count: usize = 0; + for (arg) |char| { + if (char == '\\') { + backslash_count += 1; + } else if (char == '"') { + // Double all backslashes before quote, then escape the quote + // N backslashes + " -> 2N backslashes + \" + for (0..backslash_count * 2) |_| try cmd_builder.append('\\'); + backslash_count = 0; + try cmd_builder.appendSlice("\\\""); + } else { + // Emit accumulated backslashes as-is (not before a quote) + for (0..backslash_count) |_| try cmd_builder.append('\\'); + backslash_count = 0; + try cmd_builder.append(char); + } + } + // Double any trailing backslashes before closing quote + for (0..backslash_count * 2) |_| try cmd_builder.append('\\'); + try cmd_builder.append('"'); } /// Run child process using Windows handle inheritance (idiomatic Windows approach) -fn runWithWindowsHandleInheritance(gpa: Allocator, exe_path: []const u8, shm_handle: SharedMemoryHandle) !void { +fn runWithWindowsHandleInheritance(ctx: *CliContext, exe_path: []const u8, shm_handle: SharedMemoryHandle, app_args: []const []const u8) (CliError || error{OutOfMemory})!void { // Make the shared memory handle inheritable if (windows.SetHandleInformation(@ptrCast(shm_handle.fd), windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT) == 0) { - std.log.err("Failed to set handle as inheritable\n", .{}); - return error.HandleInheritanceFailed; + return ctx.fail(.{ .shared_memory_failed = .{ + .operation = "set handle inheritable", + .err = error.HandleInheritanceFailed, + } }); } // Convert paths to Windows wide strings - const exe_path_w = try std.unicode.utf8ToUtf16LeAllocZ(gpa, exe_path); - defer gpa.free(exe_path_w); + const exe_path_w = std.unicode.utf8ToUtf16LeAllocZ(ctx.arena, exe_path) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + error.InvalidUtf8 => return ctx.fail(.{ .child_process_spawn_failed = .{ + .command = exe_path, + .err = err, + } }), + }; - const cwd = try std.fs.cwd().realpathAlloc(gpa, "."); - defer gpa.free(cwd); - const cwd_w = try std.unicode.utf8ToUtf16LeAllocZ(gpa, cwd); - defer gpa.free(cwd_w); + const cwd = std.fs.cwd().realpathAlloc(ctx.arena, ".") catch { + return ctx.fail(.{ .directory_not_found = .{ + .path = ".", + } }); + }; + const cwd_w = std.unicode.utf8ToUtf16LeAllocZ(ctx.arena, cwd) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + error.InvalidUtf8 => return ctx.fail(.{ .directory_not_found = .{ + .path = cwd, + } }), + }; - // Create command line with handle and size as arguments + // Create command line with handle and size as arguments, plus any app arguments const handle_uint = @intFromPtr(shm_handle.fd); - const cmd_line = try std.fmt.allocPrintZ(gpa, "\"{s}\" {} {}", .{ exe_path, handle_uint, shm_handle.size }); - defer gpa.free(cmd_line); - const cmd_line_w = try std.unicode.utf8ToUtf16LeAllocZ(gpa, cmd_line); - defer gpa.free(cmd_line_w); + + // Build command line string with proper quoting for Windows + var cmd_builder = std.array_list.Managed(u8).initCapacity(ctx.gpa, 256) catch { + return error.OutOfMemory; + }; + defer cmd_builder.deinit(); + try cmd_builder.writer().print("\"{s}\" {} {}", .{ exe_path, handle_uint, shm_handle.size }); + for (app_args) |arg| { + try cmd_builder.append(' '); + try appendWindowsQuotedArg(&cmd_builder, arg); + } + try cmd_builder.append(0); // null terminator for sentinel + + const cmd_line = cmd_builder.items[0 .. cmd_builder.items.len - 1 :0]; + const cmd_line_w = std.unicode.utf8ToUtf16LeAllocZ(ctx.arena, cmd_line) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + error.InvalidUtf8 => return ctx.fail(.{ .child_process_spawn_failed = .{ + .command = exe_path, + .err = err, + } }), + }; // Set up process creation structures var startup_info = std.mem.zeroes(windows.STARTUPINFOW); @@ -625,86 +1326,202 @@ fn runWithWindowsHandleInheritance(gpa: Allocator, exe_path: []const u8, shm_han ); if (success == 0) { - std.log.err("CreateProcessW failed\n", .{}); - return error.ProcessCreationFailed; + return ctx.fail(.{ .child_process_spawn_failed = .{ + .command = exe_path, + .err = error.ProcessCreationFailed, + } }); } // Child process spawned successfully // Wait for the child process to complete + std.log.debug("Waiting for child process to complete: {s}", .{exe_path}); const wait_result = windows.WaitForSingleObject(process_info.hProcess, windows.INFINITE); if (wait_result != 0) { // WAIT_OBJECT_0 = 0 - std.log.err("WaitForSingleObject failed or timed out\n", .{}); + // Clean up handles before returning + _ = ipc.platform.windows.CloseHandle(process_info.hProcess); + _ = ipc.platform.windows.CloseHandle(process_info.hThread); + return ctx.fail(.{ .child_process_wait_failed = .{ + .command = exe_path, + .err = error.ProcessWaitFailed, + } }); + } + + // Get the exit code + var exit_code: windows.DWORD = undefined; + if (windows.GetExitCodeProcess(process_info.hProcess, &exit_code) == 0) { + // Clean up handles before returning + _ = ipc.platform.windows.CloseHandle(process_info.hProcess); + _ = ipc.platform.windows.CloseHandle(process_info.hThread); + return ctx.fail(.{ .child_process_wait_failed = .{ + .command = exe_path, + .err = error.ProcessExitCodeFailed, + } }); } // Clean up process handles _ = ipc.platform.windows.CloseHandle(process_info.hProcess); _ = ipc.platform.windows.CloseHandle(process_info.hThread); + + // On Windows, clean up temp files after the child process exits. + // (Unlike Unix, Windows locks files while they're being executed) + if (std.fs.path.dirname(exe_path)) |temp_dir_path| { + compile.CacheCleanup.deleteTempDir(ctx.arena, temp_dir_path); + std.log.debug("Cleaned up temp directory: {s}", .{temp_dir_path}); + } + + // Check exit code and propagate to parent + if (exit_code != 0) { + std.log.debug("Child process {s} exited with code: {}", .{ exe_path, exit_code }); + if (exit_code == 0xC0000005) { // STATUS_ACCESS_VIOLATION + const result = platform_validation.targets_validator.ValidationResult{ + .process_crashed = .{ .exit_code = exit_code, .is_access_violation = true }, + }; + _ = platform_validation.renderValidationError(ctx.gpa, result, ctx.io.stderr()); + } else if (exit_code >= 0xC0000000) { // NT status codes for exceptions + const result = platform_validation.targets_validator.ValidationResult{ + .process_crashed = .{ .exit_code = exit_code, .is_access_violation = false }, + }; + _ = platform_validation.renderValidationError(ctx.gpa, result, ctx.io.stderr()); + } + // Propagate the exit code (truncated to u8 for compatibility) + std.process.exit(@truncate(exit_code)); + } + + std.log.debug("Child process completed successfully", .{}); } /// Run child process using POSIX file descriptor inheritance (existing approach for Unix) -fn runWithPosixFdInheritance(gpa: Allocator, exe_path: []const u8, shm_handle: SharedMemoryHandle, cache_manager: *CacheManager) !void { - // Get cache directory for temporary files - const temp_cache_dir = cache_manager.config.getTempDir(gpa) catch |err| { - std.log.err("Failed to get temp cache directory: {}\n", .{err}); - return err; +/// The exe_path should already be in a unique temp directory created by createUniqueTempDir. +fn runWithPosixFdInheritance(ctx: *CliContext, exe_path: []const u8, shm_handle: SharedMemoryHandle, app_args: []const []const u8) (CliError || error{OutOfMemory})!void { + // Write the coordination file (.txt) next to the executable + // The executable is already in a unique temp directory + std.log.debug("Writing fd coordination file for: {s}", .{exe_path}); + writeFdCoordinationFile(ctx, exe_path, shm_handle) catch |err| { + return ctx.fail(.{ .file_write_failed = .{ + .path = exe_path, + .err = err, + } }); }; - defer gpa.free(temp_cache_dir); + std.log.debug("Coordination file written successfully", .{}); - // Ensure temp cache directory exists - std.fs.cwd().makePath(temp_cache_dir) catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => { - std.log.err("Failed to create temp cache directory: {}\n", .{err}); - return err; - }, + // Configure fd inheritance - clear FD_CLOEXEC so child process inherits the fd + // Use std.posix.fcntl which properly handles the variadic C function. + const current_flags = std.posix.fcntl(shm_handle.fd, std.posix.F.GETFD, 0) catch |err| { + return ctx.fail(.{ .shared_memory_failed = .{ + .operation = "get fd flags", + .err = err, + } }); }; - // Create temporary directory structure for fd communication - const temp_exe_path = createTempDirStructure(gpa, exe_path, shm_handle, temp_cache_dir) catch |err| { - std.log.err("Failed to create temp dir structure: {}\n", .{err}); - return err; + // Clear FD_CLOEXEC - the flag value is 1 + const new_flags = current_flags & ~@as(usize, 1); + _ = std.posix.fcntl(shm_handle.fd, std.posix.F.SETFD, new_flags) catch |err| { + return ctx.fail(.{ .shared_memory_failed = .{ + .operation = "set fd flags", + .err = err, + } }); }; - defer gpa.free(temp_exe_path); - // Configure fd inheritance - var flags = posix.fcntl(shm_handle.fd, posix.F_GETFD, 0); - if (flags < 0) { - std.log.err("Failed to get fd flags: {}\n", .{c._errno().*}); - return error.FdConfigFailed; + // Debug-only verification that fd flags were actually cleared + if (comptime builtin.mode == .Debug) { + const verify_flags = std.posix.fcntl(shm_handle.fd, std.posix.F.GETFD, 0) catch |err| { + return ctx.fail(.{ .shared_memory_failed = .{ + .operation = "verify fd flags", + .err = err, + } }); + }; + if ((verify_flags & 1) != 0) { + return ctx.fail(.{ .shared_memory_failed = .{ + .operation = "clear FD_CLOEXEC", + .err = error.FdConfigFailed, + } }); + } + std.log.debug("fd={} FD_CLOEXEC cleared successfully", .{shm_handle.fd}); } - flags &= ~@as(c_int, posix.FD_CLOEXEC); - - if (posix.fcntl(shm_handle.fd, posix.F_SETFD, flags) < 0) { - std.log.err("Failed to set fd flags: {}\n", .{c._errno().*}); - return error.FdConfigFailed; + // Build argv slice using arena allocator (memory lives until arena is freed) + const argv = ctx.arena.alloc([]const u8, 1 + app_args.len) catch { + return error.OutOfMemory; + }; + argv[0] = exe_path; + for (app_args, 0..) |arg, i| { + argv[1 + i] = arg; } // Run the interpreter as a child process from the temp directory - var child = std.process.Child.init(&.{temp_exe_path}, gpa); - child.cwd = std.fs.cwd().realpathAlloc(gpa, ".") catch |err| { - std.log.err("Failed to get current directory: {}\n", .{err}); - return err; + var child = std.process.Child.init(argv, ctx.gpa); + child.cwd = std.fs.cwd().realpathAlloc(ctx.arena, ".") catch { + return ctx.fail(.{ .directory_not_found = .{ + .path = ".", + } }); }; - defer gpa.free(child.cwd.?); // Forward stdout and stderr child.stdout_behavior = .Inherit; child.stderr_behavior = .Inherit; // Spawn the child process + std.log.debug("Spawning child process: {s} with {} app args", .{ exe_path, app_args.len }); + std.log.debug("Child process working directory: {s}", .{child.cwd.?}); child.spawn() catch |err| { - std.log.err("Failed to spawn {s}: {}\n", .{ exe_path, err }); - return err; + return ctx.fail(.{ .child_process_spawn_failed = .{ + .command = exe_path, + .err = err, + } }); }; - // Child process spawned successfully + std.log.debug("Child process spawned successfully (PID: {})", .{child.id}); // Wait for child to complete - _ = child.wait() catch |err| { - std.log.err("Failed waiting for child process: {}\n", .{err}); - return err; + const term = child.wait() catch |err| { + return ctx.fail(.{ .child_process_wait_failed = .{ + .command = exe_path, + .err = err, + } }); }; + + // Clean up temp files after child has exited. + // We wait until after child exits because the child needs to read the coordination + // file to find the shared memory before it can run. + // The background cleanup thread will also clean up old temp directories. + if (std.fs.path.dirname(exe_path)) |temp_dir_path| { + compile.CacheCleanup.deleteTempDir(ctx.arena, temp_dir_path); + std.log.debug("Cleaned up temp directory: {s}", .{temp_dir_path}); + } + + // Check the termination status + switch (term) { + .Exited => |exit_code| { + if (exit_code == 0) { + std.log.debug("Child process completed successfully", .{}); + } else { + // Propagate the exit code from the child process to our parent + std.log.debug("Child process {s} exited with code: {}", .{ exe_path, exit_code }); + std.process.exit(exit_code); + } + }, + .Signal => |signal| { + std.log.debug("Child process {s} killed by signal: {}", .{ exe_path, signal }); + const result = platform_validation.targets_validator.ValidationResult{ + .process_signaled = .{ .signal = signal }, + }; + _ = platform_validation.renderValidationError(ctx.gpa, result, ctx.io.stderr()); + // Standard POSIX convention: exit with 128 + signal number + std.process.exit(128 +| @as(u8, @truncate(signal))); + }, + .Stopped => |signal| { + return ctx.fail(.{ .child_process_signaled = .{ + .command = exe_path, + .signal = signal, + } }); + }, + .Unknown => |status| { + return ctx.fail(.{ .child_process_failed = .{ + .command = exe_path, + .exit_code = status, + } }); + }, + } } /// Handle for cross-platform shared memory operations. @@ -715,6 +1532,14 @@ pub const SharedMemoryHandle = struct { size: usize, }; +/// Result of setting up shared memory with type checking information. +/// Contains both the shared memory handle for the compiled modules and +/// a count of type errors encountered during compilation. +pub const SharedMemoryResult = struct { + handle: SharedMemoryHandle, + error_count: usize, +}; + /// Write data to shared memory for inter-process communication. /// Creates a shared memory region and writes the data with a length prefix. /// Returns a handle that can be used to access the shared memory. @@ -739,7 +1564,6 @@ fn writeToWindowsSharedMemory(data: []const u8, total_size: usize) !SharedMemory @intCast(total_size), null, // Anonymous - no name needed for handle inheritance ) orelse { - std.log.err("Failed to create shared memory mapping\n", .{}); return error.SharedMemoryCreateFailed; }; @@ -770,10 +1594,11 @@ fn writeToWindowsSharedMemory(data: []const u8, total_size: usize) !SharedMemory }; } -/// Set up shared memory with a compiled ModuleEnv from a Roc file. -/// This parses, canonicalizes, and type-checks the Roc file, with the resulting ModuleEnv +/// Set up shared memory with compiled ModuleEnvs from a Roc file and its platform modules. +/// This parses, canonicalizes, and type-checks all modules, with the resulting ModuleEnvs /// ending up in shared memory because all allocations were done into shared memory. -pub fn setupSharedMemoryWithModuleEnv(gpa: std.mem.Allocator, roc_file_path: []const u8) !SharedMemoryHandle { +/// Platform type modules have their e_anno_only expressions converted to e_hosted_lambda. +pub fn setupSharedMemoryWithModuleEnv(ctx: *CliContext, roc_file_path: []const u8, allow_errors: bool) !SharedMemoryResult { // Create shared memory with SharedMemoryAllocator const page_size = try SharedMemoryAllocator.getSystemPageSize(); var shm = try SharedMemoryAllocator.create(SHARED_MEMORY_SIZE, page_size); @@ -781,103 +1606,1630 @@ pub fn setupSharedMemoryWithModuleEnv(gpa: std.mem.Allocator, roc_file_path: []c const shm_allocator = shm.allocator(); - // Allocate space for the offset value at the beginning - const offset_ptr = try shm_allocator.alloc(u64, 1); - // Also store the canonicalized expression index for the child to evaluate - const expr_idx_ptr = try shm_allocator.alloc(u32, 1); + // Load builtin modules + var builtin_modules = try eval.BuiltinModules.init(ctx.gpa); + defer builtin_modules.deinit(); - // Store the base address of the shared memory mapping (for ASLR-safe relocation) - // The child will calculate the offset from its own base address - const shm_base_addr = @intFromPtr(shm.base_ptr); - offset_ptr[0] = shm_base_addr; + // If the roc file path has no directory component (e.g., "app.roc"), use current directory + const app_dir = std.fs.path.dirname(roc_file_path) orelse "."; - // Allocate and store a pointer to the ModuleEnv - const env_ptr = try shm_allocator.create(ModuleEnv); + const platform_spec = try extractPlatformSpecFromApp(ctx, roc_file_path); - // Read the actual Roc file - const roc_file = std.fs.cwd().openFile(roc_file_path, .{}) catch |err| { - std.log.err("Failed to open Roc file '{s}': {}\n", .{ roc_file_path, err }); - return error.FileNotFound; + // Check for absolute paths and reject them early + try validatePlatformSpec(ctx, platform_spec); + + // Resolve platform path based on type: + // - Relative paths (./...) -> join with app directory + // - URL paths (http/https) -> resolve to cached package main.roc + // - Other paths -> null (not supported) + // Note: All paths use arena allocator so no manual freeing is needed. + const platform_main_path: ?[]const u8 = if (std.mem.startsWith(u8, platform_spec, "./") or std.mem.startsWith(u8, platform_spec, "../")) + try std.fs.path.join(ctx.arena, &[_][]const u8{ app_dir, platform_spec }) + else if (std.mem.startsWith(u8, platform_spec, "http://") or std.mem.startsWith(u8, platform_spec, "https://")) blk: { + // URL platform - resolve to cached package path + const platform_paths = resolveUrlPlatform(ctx, platform_spec) catch |err| switch (err) { + error.CliError => break :blk null, + error.OutOfMemory => return error.OutOfMemory, + }; + break :blk platform_paths.platform_source_path; + } else null; + + // Get the platform directory from the resolved path + const platform_dir: ?[]const u8 = if (platform_main_path) |p| + std.fs.path.dirname(p) orelse return error.InvalidPlatformPath + else + null; + + // Extract exposed modules from the platform header (if platform exists) + var exposed_modules = std.ArrayList([]const u8).empty; + defer exposed_modules.deinit(ctx.gpa); + + var has_platform = false; + if (platform_main_path) |pmp| { + has_platform = true; + extractExposedModulesFromPlatform(ctx, pmp, &exposed_modules) catch { + // Platform file not found or couldn't be parsed - continue without platform modules + has_platform = false; + }; + } + + // IMPORTANT: Create header FIRST before any module compilation. + // The interpreter_shim expects the Header to be at FIRST_ALLOC_OFFSET (504). + // If we compile modules first, they would occupy that offset and break + // shared memory layout assumptions. + const Header = struct { + parent_base_addr: u64, + module_count: u32, + entry_count: u32, + def_indices_offset: u64, + module_envs_offset: u64, + /// Offset to platform's main.roc env (0 if no platform, entry points are in app) + platform_main_env_offset: u64, + /// Offset to app env (always present, used for e_lookup_required resolution) + app_env_offset: u64, }; - defer roc_file.close(); - // Read the entire file into shared memory - const file_size = try roc_file.getEndPos(); - const source = try shm_allocator.alloc(u8, @intCast(file_size)); - _ = try roc_file.read(source); + const header_ptr = try shm_allocator.create(Header); + const shm_base_addr = @intFromPtr(shm.base_ptr); + header_ptr.parent_base_addr = shm_base_addr; - // Extract module name from the file path - const basename = std.fs.path.basename(roc_file_path); - const module_name = try shm_allocator.dupe(u8, basename); + // Module count = 1 (app) + number of platform modules + const total_module_count: u32 = 1 + @as(u32, @intCast(exposed_modules.items.len)); + header_ptr.module_count = total_module_count; - var env = try ModuleEnv.init(shm_allocator, source); - env.common.source = source; - env.module_name = module_name; - try env.common.calcLineStarts(shm_allocator); + // Allocate array for module env offsets + const module_env_offsets_ptr = try shm_allocator.alloc(u64, total_module_count); + const module_envs_offset_location = @intFromPtr(module_env_offsets_ptr.ptr) - @intFromPtr(shm.base_ptr); + header_ptr.module_envs_offset = module_envs_offset_location; - // Parse the source code as a full module - var parse_ast = try parse.parse(&env.common, gpa); + // Compile platform sibling modules FIRST (Stdout, Stderr, Stdin, etc.) + // This must happen before platform main.roc so that when main.roc is canonicalized, + // we can pass the sibling modules to module_envs and validate imports correctly. + // + // Modules are automatically sorted by their import dependencies using topological sort. + // If module A imports module B, B will be compiled before A regardless of the order + // in the platform's exposes list. + // platform_dir is guaranteed to be non-null if exposed_modules is non-empty + // because we only populate exposed_modules when platform_main_path is non-null + const plat_dir = platform_dir orelse unreachable; + const sorted_modules = sortPlatformModulesByDependency( + ctx, + exposed_modules.items, + plat_dir, + ) catch |err| { + if (err == error.CyclicDependency) { + std.log.err("Circular dependency detected in platform modules", .{}); + } + return err; + }; + defer ctx.gpa.free(sorted_modules); - // Empty scratch space (required before canonicalization) - parse_ast.store.emptyScratch(); + var platform_env_ptrs = try ctx.gpa.alloc(*ModuleEnv, sorted_modules.len); + defer ctx.gpa.free(platform_env_ptrs); - // Initialize CIR fields in ModuleEnv - try env.initCIRFields(shm_allocator, module_name); + if (comptime trace_modules) { + std.debug.print("[TRACE-MODULES] === IPC Mode: Compiling Platform Modules ===\n", .{}); + } - // Create canonicalizer - var canonicalizer = try Can.init(&env, &parse_ast, null); + for (sorted_modules, 0..) |module_name, i| { + const module_filename = try std.fmt.allocPrint(ctx.gpa, "{s}.roc", .{module_name}); + defer ctx.gpa.free(module_filename); - // Canonicalize the entire module - try canonicalizer.canonicalizeFile(); + const module_path = try std.fs.path.join(ctx.gpa, &[_][]const u8{ plat_dir, module_filename }); + defer ctx.gpa.free(module_path); - // Find the "main" definition in the module - // Look through all definitions to find one named "main" - var main_expr_idx: ?u32 = null; - const defs = env.store.sliceDefs(env.all_defs); - for (defs) |def_idx| { - const def = env.store.getDef(def_idx); - const pattern = env.store.getPattern(def.pattern); - if (pattern == .assign) { - const ident_idx = pattern.assign.ident; - const ident_text = env.getIdent(ident_idx); - if (std.mem.eql(u8, ident_text, "main")) { - main_expr_idx = @intFromEnum(def.expr); - break; + if (comptime trace_modules) { + std.debug.print("[TRACE-MODULES] Compiling platform module {d}: \"{s}\" at {s}\n", .{ i, module_name, module_path }); + } + + // Pass previously compiled sibling modules so this module can resolve imports to them. + // This enables transitive module calls (e.g., a module `Helper` imports `Core`, then calls `Core.wrap`). + const sibling_modules = platform_env_ptrs[0..i]; + const module_env_ptr = try compileModuleToSharedMemory( + ctx, + module_path, + module_name, // Use just "Stdout" (not "Stdout.roc") so type-module detection works + shm_allocator, + &builtin_modules, + sibling_modules, + ); + + // Store platform modules at indices 0..N-2, app will be at N-1 + module_env_offsets_ptr[i] = @intFromPtr(module_env_ptr) - @intFromPtr(shm.base_ptr); + platform_env_ptrs[i] = module_env_ptr; + } + + // NOW compile platform main.roc AFTER sibling modules so we can pass them to module_envs. + // This allows the canonicalizer to validate that imports of Stdout, Stderr, etc. are valid. + var platform_main_env: ?*ModuleEnv = null; + if (has_platform) { + if (comptime trace_modules) { + std.debug.print("[TRACE-MODULES] Compiling platform main: {s}\n", .{platform_main_path.?}); + } + + // Cast []*ModuleEnv to []const *ModuleEnv for the function parameter + const const_platform_env_ptrs: []const *ModuleEnv = platform_env_ptrs; + // platform_main_path is guaranteed non-null when has_platform is true + platform_main_env = compileModuleToSharedMemory( + ctx, + platform_main_path.?, + "main.roc", + shm_allocator, + &builtin_modules, + const_platform_env_ptrs, + ) catch null; + } + + // Collect and sort all hosted functions globally, then assign indices + if (platform_env_ptrs.len > 0) { + const HostedCompiler = can.HostedCompiler; + var all_hosted_fns = std.ArrayList(HostedCompiler.HostedFunctionInfo).empty; + defer all_hosted_fns.deinit(ctx.gpa); + + // Collect from all platform modules + for (platform_env_ptrs) |platform_env| { + var module_fns = try HostedCompiler.collectAndSortHostedFunctions(platform_env); + defer module_fns.deinit(platform_env.gpa); + + for (module_fns.items) |fn_info| { + try all_hosted_fns.append(ctx.gpa, fn_info); + } + } + + // Sort globally + const SortContext = struct { + pub fn lessThan(_: void, a: HostedCompiler.HostedFunctionInfo, b: HostedCompiler.HostedFunctionInfo) bool { + return std.mem.order(u8, a.name_text, b.name_text) == .lt; + } + }; + std.mem.sort(HostedCompiler.HostedFunctionInfo, all_hosted_fns.items, {}, SortContext.lessThan); + + // Deduplicate + var write_idx: usize = 0; + for (all_hosted_fns.items, 0..) |fn_info, read_idx| { + if (write_idx == 0 or !std.mem.eql(u8, all_hosted_fns.items[write_idx - 1].name_text, fn_info.name_text)) { + if (write_idx != read_idx) { + all_hosted_fns.items[write_idx] = fn_info; + } + write_idx += 1; + } else { + ctx.gpa.free(fn_info.name_text); + } + } + all_hosted_fns.shrinkRetainingCapacity(write_idx); + + // Reassign global indices + for (platform_env_ptrs) |platform_env| { + const all_defs = platform_env.store.sliceDefs(platform_env.all_defs); + for (all_defs) |def_idx| { + const def = platform_env.store.getDef(def_idx); + const expr = platform_env.store.getExpr(def.expr); + + if (expr == .e_hosted_lambda) { + const hosted = expr.e_hosted_lambda; + const local_name = platform_env.getIdent(hosted.symbol_name); + + var plat_module_name = platform_env.module_name; + if (std.mem.endsWith(u8, plat_module_name, ".roc")) { + plat_module_name = plat_module_name[0 .. plat_module_name.len - 4]; + } + const qualified_name = try std.fmt.allocPrint(ctx.gpa, "{s}.{s}", .{ plat_module_name, local_name }); + defer ctx.gpa.free(qualified_name); + + const stripped_name = if (std.mem.endsWith(u8, qualified_name, "!")) + qualified_name[0 .. qualified_name.len - 1] + else + qualified_name; + + for (all_hosted_fns.items, 0..) |fn_info, idx| { + if (std.mem.eql(u8, fn_info.name_text, stripped_name)) { + const expr_node_idx = @as(@TypeOf(platform_env.store.nodes).Idx, @enumFromInt(@intFromEnum(def.expr))); + var expr_node = platform_env.store.nodes.get(expr_node_idx); + expr_node.data_2 = @intCast(idx); + platform_env.store.nodes.set(expr_node_idx, expr_node); + break; + } + } + } } } } - // Store the main expression index for the child - expr_idx_ptr[0] = main_expr_idx orelse { - std.log.err("No 'main' definition found in module\n", .{}); - return error.NoMainFunction; + // Now compile the app module + if (comptime trace_modules) { + std.debug.print("[TRACE-MODULES] Compiling app: {s}\n", .{roc_file_path}); + } + + const app_env_ptr = try shm_allocator.create(ModuleEnv); + + const app_file = std.fs.cwd().openFile(roc_file_path, .{}) catch |err| { + const problem: CliProblem = switch (err) { + error.FileNotFound => .{ .file_not_found = .{ + .path = roc_file_path, + .context = .source_file, + } }, + else => .{ .file_read_failed = .{ + .path = roc_file_path, + .err = err, + } }, + }; + renderProblem(ctx.gpa, ctx.io.stderr(), problem); + return error.FileNotFound; + }; + defer app_file.close(); + + const app_file_size = try app_file.getEndPos(); + var app_source = try shm_allocator.alloc(u8, @intCast(app_file_size)); + _ = try app_file.read(app_source); + // Normalize line endings (CRLF -> LF) for consistent cross-platform parsing. + // SharedMemoryAllocator is a bump allocator, so normalize in-place and keep any trailing bytes unused. + app_source = base.source_utils.normalizeLineEndings(app_source); + + const app_basename = std.fs.path.basename(roc_file_path); + const app_module_name = try shm_allocator.dupe(u8, app_basename); + + var app_env = try ModuleEnv.init(shm_allocator, app_source); + app_env.common.source = app_source; + app_env.module_name = app_module_name; + try app_env.common.calcLineStarts(shm_allocator); + + var error_count: usize = 0; + + var app_parse_ast = try parse.parse(&app_env.common, ctx.gpa); + defer app_parse_ast.deinit(ctx.gpa); + if (app_parse_ast.hasErrors()) { + const stderr = ctx.io.stderr(); + for (app_parse_ast.tokenize_diagnostics.items) |diagnostic| { + error_count += 1; + var report = app_parse_ast.tokenizeDiagnosticToReport(diagnostic, ctx.gpa, roc_file_path) catch continue; + defer report.deinit(); + reporting.renderReportToTerminal(&report, stderr, ColorPalette.ANSI, reporting.ReportingConfig.initColorTerminal()) catch continue; + } + for (app_parse_ast.parse_diagnostics.items) |diagnostic| { + error_count += 1; + var report = app_parse_ast.parseDiagnosticToReport(&app_env.common, diagnostic, ctx.gpa, roc_file_path) catch continue; + defer report.deinit(); + reporting.renderReportToTerminal(&report, stderr, ColorPalette.ANSI, reporting.ReportingConfig.initColorTerminal()) catch continue; + } + // If errors are not allowed then we should not move past parsing. return early and let caller handle error/exit + if (!allow_errors) { + return SharedMemoryResult{ + .handle = SharedMemoryHandle{ + .fd = shm.handle, + .ptr = shm.base_ptr, + .size = shm.getUsedSize(), + }, + .error_count = error_count, + }; + } + } + + app_parse_ast.store.emptyScratch(); + try app_env.initCIRFields(app_module_name); + + var app_module_envs_map = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(ctx.gpa); + defer app_module_envs_map.deinit(); + + try Can.populateModuleEnvs( + &app_module_envs_map, + &app_env, + builtin_modules.builtin_module.env, + builtin_modules.builtin_indices, + ); + + for (platform_env_ptrs) |mod_env| { + const name = try app_env.insertIdent(base.Ident.for_text(mod_env.module_name)); + // For user/platform modules, the qualified name is just the module name itself + const qualified_ident = try app_env.insertIdent(base.Ident.for_text(mod_env.module_name)); + try app_module_envs_map.put(name, .{ .env = mod_env, .qualified_type_ident = qualified_ident }); + } + + // Add platform modules to the module envs map for canonicalization + // Two keys are needed for each platform module: + // 1. "pf.Stdout" - used during import validation (import pf.Stdout) + // 2. "Stdout" - used during expression canonicalization (Stdout.line!) + // Also set statement_idx to the actual type node index, which is needed for + // creating e_nominal_external and e_lookup_external expressions. + // Note: We iterate over sorted_modules to match the order in platform_env_ptrs + for (sorted_modules, 0..) |module_name, i| { + const platform_env = platform_env_ptrs[i]; + // For platform modules (type modules), the qualified type name is just the type name. + // Type modules like Stdout.roc store associated items as "Stdout.line!" (not "Stdout.roc.Stdout.line!") + // because processTypeDeclFirstPass uses parent_name=null for top-level types. + // Insert into app_env (calling module) since Ident.Idx values are not transferable between stores. + const type_qualified_ident = try app_env.insertIdent(base.Ident.for_text(module_name)); + + // Look up the type in the platform module's exposed_items to get the actual node index + const type_ident_in_platform = platform_env.common.findIdent(module_name) orelse { + return ctx.fail(.{ .missing_type_in_module = .{ + .module_name = module_name, + .type_name = module_name, + } }); + }; + const type_node_idx = platform_env.getExposedNodeIndexById(type_ident_in_platform) orelse { + return ctx.fail(.{ .missing_type_in_module = .{ + .module_name = module_name, + .type_name = module_name, + } }); + }; + + const auto_type = Can.AutoImportedType{ + .env = platform_env, + .statement_idx = @enumFromInt(type_node_idx), // actual type node index for e_lookup_external + .qualified_type_ident = type_qualified_ident, + }; + + // Add with qualified name key (for import validation: "pf.Stdout") + const qualified_name = try std.fmt.allocPrint(ctx.gpa, "pf.{s}", .{module_name}); + defer ctx.gpa.free(qualified_name); + const qualified_ident = try app_env.insertIdent(base.Ident.for_text(qualified_name)); + try app_module_envs_map.put(qualified_ident, auto_type); + + // Add with unaliased name key (for expression canonicalization: "Stdout") + const module_ident = try app_env.insertIdent(base.Ident.for_text(module_name)); + try app_module_envs_map.put(module_ident, auto_type); + + // Add with resolved module name key (for after alias resolution: "Stdout.roc") + // The import system resolves "pf.Stdout" to "Stdout.roc", so scopeLookupModule + // returns "Stdout.roc" which is then used to look up in module_envs + const module_name_with_roc = try std.fmt.allocPrint(ctx.gpa, "{s}.roc", .{module_name}); + defer ctx.gpa.free(module_name_with_roc); + const resolved_ident = try app_env.insertIdent(base.Ident.for_text(module_name_with_roc)); + try app_module_envs_map.put(resolved_ident, auto_type); + } + + var app_canonicalizer = try Can.init(&app_env, &app_parse_ast, &app_module_envs_map); + defer app_canonicalizer.deinit(); + + try app_canonicalizer.canonicalizeFile(); + try app_canonicalizer.validateForExecution(); + + if (app_env.exports.span.len == 0) { + return ctx.fail(.{ .no_exports_found = .{ .path = roc_file_path } }); + } + + // Store app env at the last index (N-1, after platform modules at 0..N-2) + module_env_offsets_ptr[total_module_count - 1] = @intFromPtr(app_env_ptr) - @intFromPtr(shm.base_ptr); + + // Store app env offset for e_lookup_required resolution + header_ptr.app_env_offset = @intFromPtr(app_env_ptr) - @intFromPtr(shm.base_ptr); + + // Entry points are defined in the platform's `provides` section. + // The platform wraps app-provided functions (from `requires`) and exports them for the host. + // For example: `provides { main_for_host!: "main" }` where `main_for_host! = main!` + const platform_env = platform_main_env orelse { + const result = platform_validation.targets_validator.ValidationResult{ + .no_platform_found = .{ .app_path = roc_file_path }, + }; + _ = platform_validation.renderValidationError(ctx.gpa, result, ctx.io.stderr()); + return error.NoPlatformFound; + }; + const exports_slice = platform_env.store.sliceDefs(platform_env.exports); + if (exports_slice.len == 0) { + return ctx.fail(.{ .no_exports_found = .{ .path = platform_env.module_name } }); + } + + // Store platform env offset for entry point lookups + header_ptr.platform_main_env_offset = @intFromPtr(platform_env) - @intFromPtr(shm.base_ptr); + header_ptr.entry_count = @intCast(exports_slice.len); + + const def_indices_ptr = try shm_allocator.alloc(u32, exports_slice.len); + header_ptr.def_indices_offset = @intFromPtr(def_indices_ptr.ptr) - @intFromPtr(shm.base_ptr); + + for (exports_slice, 0..) |def_idx, i| { + def_indices_ptr[i] = @intFromEnum(def_idx); + } + + // Type check with all imported modules + // Use the env's module_name_idx so that nominal types' origin_module matches + // the env's identity for method resolution at runtime + const app_builtin_ctx: Check.BuiltinContext = .{ + .module_name = app_env.module_name_idx, + .bool_stmt = builtin_modules.builtin_indices.bool_type, + .try_stmt = builtin_modules.builtin_indices.try_type, + .str_stmt = builtin_modules.builtin_indices.str_type, + .builtin_module = builtin_modules.builtin_module.env, + .builtin_indices = builtin_modules.builtin_indices, }; - // Type check the module - var checker = try Check.init(shm_allocator, &env.types, &env, &.{}, &env.store.regions); - try checker.checkDefs(); + var app_imported_envs = std.ArrayList(*const ModuleEnv).empty; + defer app_imported_envs.deinit(ctx.gpa); + try app_imported_envs.append(ctx.gpa, builtin_modules.builtin_module.env); + for (platform_env_ptrs) |penv| { + try app_imported_envs.append(ctx.gpa, penv); + } - // Copy the ModuleEnv to the allocated space - env_ptr.* = env; + // Resolve imports - map each import to its index in app_imported_envs + app_env.imports.resolveImports(&app_env, app_imported_envs.items); - // Clean up the canonicalizer and parsing structures - canonicalizer.deinit(); + var app_checker = try Check.init(shm_allocator, &app_env.types, &app_env, app_imported_envs.items, &app_module_envs_map, &app_env.store.regions, app_builtin_ctx); + defer app_checker.deinit(); - // Clean up parse_ast since it was allocated with gpa, not shared memory - parse_ast.deinit(gpa); + try app_checker.checkFile(); - // Clean up checker since it was allocated with shared memory, but we need to clean up its gpa allocations - checker.deinit(); + // Check that app exports match platform requirements (if platform exists) + if (platform_main_env) |penv| { + // Build the platform-to-app ident translation map + var platform_to_app_idents = std.AutoHashMap(base.Ident.Idx, base.Ident.Idx).init(ctx.gpa); + defer platform_to_app_idents.deinit(); + + for (penv.requires_types.items.items) |required_type| { + const platform_ident_text = penv.getIdent(required_type.ident); + if (app_env.common.findIdent(platform_ident_text)) |app_ident| { + try platform_to_app_idents.put(required_type.ident, app_ident); + } + + // Also add for-clause type alias names (Model, model) to the translation map + const all_aliases = penv.for_clause_aliases.items.items; + const type_aliases_slice = all_aliases[@intFromEnum(required_type.type_aliases.start)..][0..required_type.type_aliases.count]; + for (type_aliases_slice) |alias| { + // Add alias name (e.g., "Model") - must exist in app since it's required + const alias_name_text = penv.getIdent(alias.alias_name); + if (app_env.common.findIdent(alias_name_text)) |app_ident| { + try platform_to_app_idents.put(alias.alias_name, app_ident); + } + // Add rigid name (e.g., "model") - insert it into app's ident store since + // the rigid name is a platform concept that gets copied during type processing. + // Using insert (not find) ensures the app's ident store has this name for later lookups. + const rigid_name_text = penv.getIdent(alias.rigid_name); + const app_ident = try app_env.common.insertIdent(ctx.gpa, base.Ident.for_text(rigid_name_text)); + try platform_to_app_idents.put(alias.rigid_name, app_ident); + } + } + + try app_checker.checkPlatformRequirements(penv, &platform_to_app_idents); + } + + // Render all type problems (errors and warnings) exactly as roc check would + // Count errors so the caller can decide whether to proceed with execution + // Skip rendering in test mode to avoid polluting test output + error_count += if (!builtin.is_test) + renderTypeProblems(ctx, &app_checker, &app_env, roc_file_path) + else + 0; + + app_env_ptr.* = app_env; - // Update the header with used size shm.updateHeader(); - // Return the shared memory handle from SharedMemoryAllocator - // This ensures we use the SAME shared memory region for both processes - return SharedMemoryHandle{ - .fd = shm.handle, - .ptr = shm.base_ptr, - .size = shm.getUsedSize(), + return SharedMemoryResult{ + .handle = SharedMemoryHandle{ + .fd = shm.handle, + .ptr = shm.base_ptr, + .size = shm.getUsedSize(), + }, + .error_count = error_count, + }; +} + +/// Extract exposed modules from a platform's main.roc file +fn extractExposedModulesFromPlatform(ctx: *CliContext, roc_file_path: []const u8, exposed_modules: *std.ArrayList([]const u8)) !void { + // Read the Roc file + var source = std.fs.cwd().readFileAlloc(ctx.gpa, roc_file_path, std.math.maxInt(usize)) catch return error.NoPlatformFound; + source = base.source_utils.normalizeLineEndingsRealloc(ctx.gpa, source) catch |err| { + ctx.gpa.free(source); + return err; + }; + defer ctx.gpa.free(source); + + // Extract module name from the file path + const basename = std.fs.path.basename(roc_file_path); + const module_name = try ctx.arena.dupe(u8, basename); + + // Create ModuleEnv + var env = ModuleEnv.init(ctx.gpa, source) catch return error.ParseFailed; + defer env.deinit(); + + env.common.source = source; + env.module_name = module_name; + try env.common.calcLineStarts(ctx.gpa); + + // Parse the source code as a full module + var parse_ast = parse.parse(&env.common, ctx.gpa) catch return error.ParseFailed; + defer parse_ast.deinit(ctx.gpa); + + // Look for platform header in the AST + const file_node = parse_ast.store.getFile(); + const header = parse_ast.store.getHeader(file_node.header); + + // Check if this is a platform file with a platform header + switch (header) { + .platform => |platform_header| { + // Validate platform header has targets section (non-blocking warning) + // This helps platform authors know they need to add targets + _ = validatePlatformHeader(ctx, &parse_ast, roc_file_path); + + // Get the exposes collection + const exposes_coll = parse_ast.store.getCollection(platform_header.exposes); + const exposes_items = parse_ast.store.exposedItemSlice(.{ .span = exposes_coll.span }); + + // Extract all exposed module names + for (exposes_items) |item_idx| { + const item = parse_ast.store.getExposedItem(item_idx); + const token_idx = switch (item) { + .upper_ident => |ui| ui.ident, + .upper_ident_star => |uis| uis.ident, + .lower_ident => |li| li.ident, + .malformed => continue, // Skip malformed items + }; + const item_name = parse_ast.resolve(token_idx); + try exposed_modules.append(ctx.gpa, try ctx.arena.dupe(u8, item_name)); + } + }, + else => { + return error.NotPlatformFile; + }, + } +} + +/// Validate a platform header and report any errors/warnings +/// Returns true if valid, false if there are validation issues +/// This currently only warns about missing targets sections - it doesn't block compilation +fn validatePlatformHeader(ctx: *CliContext, parse_ast: *const parse.AST, platform_path: []const u8) bool { + const validation_result = targets_validator.validatePlatformHasTargets(parse_ast.*, platform_path); + + switch (validation_result) { + .valid => return true, + else => { + // Create and render the validation report + var report = targets_validator.createValidationReport(ctx.gpa, validation_result) catch { + std.log.warn("Platform at {s} is missing targets section", .{platform_path}); + return false; + }; + defer report.deinit(); + + // Render to stderr + if (!builtin.is_test) { + reporting.renderReportToTerminal(&report, ctx.io.stderr(), .ANSI, reporting.ReportingConfig.initColorTerminal()) catch {}; + } + return false; + }, + } +} + +/// Extract the names of local modules that a given module file imports. +/// Only returns unqualified imports (e.g., "Core"), not qualified ones (e.g., "pf.Stdout"). +/// This is used to determine dependency ordering for platform modules. +/// +/// Parameters: +/// allocs: Allocator bundle for temporary allocations +/// module_path: Absolute path to the .roc module file +/// available_modules: Set of module names to filter against (only return imports that are in this set) +/// +/// Returns: Slice of imported module names that are in the available_modules set +/// Caller owns the returned memory (allocated with ctx.gpa). +fn extractModuleImports( + ctx: *CliContext, + module_path: []const u8, + available_modules: []const []const u8, +) ![][]const u8 { + // Read source file + const source = std.fs.cwd().readFileAlloc(ctx.gpa, module_path, std.math.maxInt(usize)) catch |err| { + std.log.warn("Failed to read module file {s}: {}", .{ module_path, err }); + return &[_][]const u8{}; + }; + defer ctx.gpa.free(source); + + // Extract module name from the file path + const basename = std.fs.path.basename(module_path); + const module_name = basename[0 .. basename.len - 4]; // Remove .roc + + // Create ModuleEnv and parse + var env = ModuleEnv.init(ctx.gpa, source) catch { + return &[_][]const u8{}; + }; + defer env.deinit(); + + env.common.source = source; + env.module_name = module_name; + try env.common.calcLineStarts(ctx.gpa); + + // Parse the source + var parse_ast = parse.parse(&env.common, ctx.gpa) catch { + return &[_][]const u8{}; + }; + defer parse_ast.deinit(ctx.gpa); + parse_ast.store.emptyScratch(); + + // Initialize CIR fields (needed for canonicalization) + try env.initCIRFields(module_name); + + // Create a minimal module_envs map (just builtins would go here, but for import extraction we don't need them) + var module_envs_map = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(ctx.gpa); + defer module_envs_map.deinit(); + + // Canonicalize to discover imports + var canonicalizer = try Can.init(&env, &parse_ast, &module_envs_map); + defer canonicalizer.deinit(); + canonicalizer.canonicalizeFile() catch { + // Even if canonicalization fails, we might have discovered some imports + }; + + // Extract imports from env.imports.imports + const import_count = env.imports.imports.items.items.len; + var result = std.ArrayList([]const u8).empty; + errdefer { + for (result.items) |item| ctx.gpa.free(item); + result.deinit(ctx.gpa); + } + + for (env.imports.imports.items.items[0..import_count]) |str_idx| { + const import_name = env.common.getString(str_idx); + + // Skip qualified imports (e.g., "pf.Stdout") - we only care about local imports + if (std.mem.indexOfScalar(u8, import_name, '.') != null) { + continue; + } + + // Skip "Builtin" - it's always available + if (std.mem.eql(u8, import_name, "Builtin")) { + continue; + } + + // Only include imports that are in the available_modules set + var found = false; + for (available_modules) |avail| { + if (std.mem.eql(u8, import_name, avail)) { + found = true; + break; + } + } + if (!found) continue; + + // Add to result (duplicate the string since env will be freed) + try result.append(ctx.gpa, try ctx.gpa.dupe(u8, import_name)); + } + + return result.toOwnedSlice(ctx.gpa); +} + +/// Sort platform modules by their import dependencies using topological sort (Kahn's algorithm). +/// Returns modules in compilation order (dependencies first, dependents last). +/// Returns error.CyclicDependency if modules have circular imports. +/// +/// Parameters: +/// allocs: Allocator bundle +/// module_names: List of module names from the platform's exposes list +/// platform_dir: Directory containing the platform modules +/// +/// Returns: Sorted list of module names +/// Caller owns the returned memory (allocated with ctx.gpa). +fn sortPlatformModulesByDependency( + ctx: *CliContext, + module_names: []const []const u8, + platform_dir: []const u8, +) ![][]const u8 { + const n = module_names.len; + + // Early return for trivial cases + if (n <= 1) { + var result = try ctx.gpa.alloc([]const u8, n); + for (module_names, 0..) |name, i| { + result[i] = name; + } + return result; + } + + // Build a name -> index map for O(1) lookups + var name_to_idx = std.StringHashMap(usize).init(ctx.gpa); + defer name_to_idx.deinit(); + for (module_names, 0..) |name, i| { + try name_to_idx.put(name, i); + } + + // Build adjacency list: adj[i] = list of modules that module i depends on (imports) + // And compute in-degree: how many modules depend on each module + var adjacency = try ctx.gpa.alloc(std.ArrayList(usize), n); + defer { + for (adjacency) |*list| list.deinit(ctx.gpa); + ctx.gpa.free(adjacency); + } + for (adjacency) |*list| { + list.* = std.ArrayList(usize).empty; + } + + var in_degree = try ctx.gpa.alloc(usize, n); + defer ctx.gpa.free(in_degree); + @memset(in_degree, 0); + + // For each module, extract its imports and build the graph + for (module_names, 0..) |name, i| { + const module_filename = try std.fmt.allocPrint(ctx.gpa, "{s}.roc", .{name}); + defer ctx.gpa.free(module_filename); + + const module_path = try std.fs.path.join(ctx.gpa, &[_][]const u8{ platform_dir, module_filename }); + defer ctx.gpa.free(module_path); + + const imports = try extractModuleImports(ctx, module_path, module_names); + defer { + for (imports) |imp| ctx.gpa.free(imp); + ctx.gpa.free(imports); + } + + // For each import, add an edge: this module depends on the imported module + for (imports) |imp| { + if (name_to_idx.get(imp)) |dep_idx| { + // Module i imports module dep_idx, so dep_idx must come before i + // Edge: dep_idx -> i (dep_idx is depended upon by i) + try adjacency[dep_idx].append(ctx.gpa, i); + in_degree[i] += 1; + + if (comptime trace_modules) { + std.debug.print("[TRACE-MODULES] Dependency: {s} imports {s}\n", .{ name, imp }); + } + } + } + } + + // Kahn's algorithm: start with modules that have no dependencies (in_degree == 0) + var queue = std.ArrayList(usize).empty; + defer queue.deinit(ctx.gpa); + + for (0..n) |i| { + if (in_degree[i] == 0) { + try queue.append(ctx.gpa, i); + } + } + + var result = try ctx.gpa.alloc([]const u8, n); + errdefer ctx.gpa.free(result); + var result_count: usize = 0; + + while (queue.items.len > 0) { + const current = queue.orderedRemove(0); + result[result_count] = module_names[current]; + result_count += 1; + + // For each module that depends on current, decrement its in-degree + for (adjacency[current].items) |dependent| { + in_degree[dependent] -= 1; + if (in_degree[dependent] == 0) { + try queue.append(ctx.gpa, dependent); + } + } + } + + // Log the sorted order + if (comptime trace_modules) { + std.debug.print("[TRACE-MODULES] Sorted compilation order: ", .{}); + for (result[0..result_count], 0..) |mod, idx| { + if (idx > 0) std.debug.print(", ", .{}); + std.debug.print("{s}", .{mod}); + } + std.debug.print("\n", .{}); + } + + // If we didn't process all modules, there's a cycle + if (result_count != n) { + // Find modules in the cycle (those with in_degree > 0) + var cycle_modules = std.ArrayList([]const u8).empty; + defer cycle_modules.deinit(ctx.gpa); + + for (0..n) |i| { + if (in_degree[i] > 0) { + try cycle_modules.append(ctx.gpa, module_names[i]); + } + } + + // Log the cycle for debugging + std.log.err("Circular dependency detected in platform modules:", .{}); + for (cycle_modules.items) |mod| { + std.log.err(" - {s}", .{mod}); + } + + ctx.gpa.free(result); + return error.CyclicDependency; + } + + return result; +} + +/// Compile a single module to shared memory (for platform modules) +fn compileModuleToSharedMemory( + ctx: *CliContext, + file_path: []const u8, + module_name_arg: []const u8, + shm_allocator: std.mem.Allocator, + builtin_modules: *eval.BuiltinModules, + additional_modules: []const *ModuleEnv, +) !*ModuleEnv { + // Read file + const file = try std.fs.cwd().openFile(file_path, .{}); + defer file.close(); + + const file_size = try file.getEndPos(); + var source = try shm_allocator.alloc(u8, @intCast(file_size)); + _ = try file.read(source); + // Normalize line endings (CRLF -> LF) for consistent cross-platform parsing. + // SharedMemoryAllocator is a bump allocator, so normalize in-place and keep any trailing bytes unused. + source = base.source_utils.normalizeLineEndings(source); + + const module_name_copy = try shm_allocator.dupe(u8, module_name_arg); + + // Initialize ModuleEnv + var env = try ModuleEnv.init(shm_allocator, source); + env.common.source = source; + env.module_name = module_name_copy; + try env.common.calcLineStarts(shm_allocator); + + // Parse + var parse_ast = try parse.parse(&env.common, ctx.gpa); + defer parse_ast.deinit(ctx.gpa); + parse_ast.store.emptyScratch(); + + // Initialize CIR + try env.initCIRFields(module_name_copy); + + // Create module_envs map + var module_envs_map = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(ctx.gpa); + defer module_envs_map.deinit(); + + try Can.populateModuleEnvs( + &module_envs_map, + &env, + builtin_modules.builtin_module.env, + builtin_modules.builtin_indices, + ); + + for (additional_modules) |mod_env| { + // Get the base module name (without .roc extension if present) + var base_module_name = mod_env.module_name; + if (std.mem.endsWith(u8, base_module_name, ".roc")) { + base_module_name = base_module_name[0 .. base_module_name.len - 4]; + } + + // Check if this module is a "type module" (defines a type with the same name as the module). + // For type modules like Core (which uses `Core := [].{ wrap = ... }`), functions are exposed + // as qualified names like "Core.wrap", so we need to set statement_idx for proper lookup. + const type_ident_in_module = mod_env.common.findIdent(base_module_name); + const type_node_idx: ?u16 = if (type_ident_in_module) |ident| + mod_env.getExposedNodeIndexById(ident) + else + null; + + const name = try env.insertIdent(base.Ident.for_text(base_module_name)); + const qualified_ident = try env.insertIdent(base.Ident.for_text(base_module_name)); + + if (type_node_idx) |node_idx| { + // This is a type module - set statement_idx for proper qualified lookup + try module_envs_map.put(name, .{ + .env = mod_env, + .statement_idx = @enumFromInt(node_idx), + .qualified_type_ident = qualified_ident, + }); + } else { + // Regular module - no statement_idx needed + try module_envs_map.put(name, .{ + .env = mod_env, + .qualified_type_ident = qualified_ident, + }); + } + + // Also add with full .roc suffix if different + if (!std.mem.eql(u8, mod_env.module_name, base_module_name)) { + const full_name = try env.insertIdent(base.Ident.for_text(mod_env.module_name)); + if (type_node_idx) |node_idx| { + try module_envs_map.put(full_name, .{ + .env = mod_env, + .statement_idx = @enumFromInt(node_idx), + .qualified_type_ident = qualified_ident, + }); + } else { + try module_envs_map.put(full_name, .{ + .env = mod_env, + .qualified_type_ident = qualified_ident, + }); + } + } + } + + // Canonicalize (without root_is_platform - we'll run HostedCompiler separately) + var canonicalizer = try Can.init(&env, &parse_ast, &module_envs_map); + defer canonicalizer.deinit(); + + try canonicalizer.canonicalizeFile(); + + // Run HostedCompiler to convert e_anno_only to e_hosted_lambda + // This is the key step for platform type modules + const HostedCompiler = can.HostedCompiler; + _ = try HostedCompiler.replaceAnnoOnlyWithHosted(&env); + + // Type check + var check_module_envs_map = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(ctx.gpa); + defer check_module_envs_map.deinit(); + + const builtin_ctx: Check.BuiltinContext = .{ + .module_name = try env.insertIdent(base.Ident.for_text(module_name_arg)), + .bool_stmt = builtin_modules.builtin_indices.bool_type, + .try_stmt = builtin_modules.builtin_indices.try_type, + .str_stmt = builtin_modules.builtin_indices.str_type, + .builtin_module = builtin_modules.builtin_module.env, + .builtin_indices = builtin_modules.builtin_indices, + }; + + // Build imported_envs array: builtins + additional modules + // This is needed for resolveImports to properly map external lookups + // (e.g., when Helper imports Core, Core must be in imported_envs) + var imported_envs_list = try std.ArrayList(*const ModuleEnv).initCapacity(ctx.gpa, 1 + additional_modules.len); + defer imported_envs_list.deinit(ctx.gpa); + imported_envs_list.appendAssumeCapacity(builtin_modules.builtin_module.env); + for (additional_modules) |mod| { + imported_envs_list.appendAssumeCapacity(mod); + } + const imported_envs = imported_envs_list.items; + + // Resolve imports - map each import to its index in imported_envs + env.imports.resolveImports(&env, imported_envs); + + var checker = try Check.init(shm_allocator, &env.types, &env, imported_envs, &check_module_envs_map, &env.store.regions, builtin_ctx); + defer checker.deinit(); + + try checker.checkFile(); + + // Allocate and return + const env_ptr = try shm_allocator.create(ModuleEnv); + env_ptr.* = env; + return env_ptr; +} + +/// Compiled module data ready for serialization. +/// Holds the ModuleEnv, source bytes, and module name needed for serialization. +const CompiledModule = struct { + env: ModuleEnv, + source: []const u8, + module_name: []const u8, + is_platform_main: bool, + is_app: bool, + /// Number of errors found during compilation (from parsing, canonicalization, type checking) + error_count: usize, +}; + +/// Result of compiling and serializing modules for embedding. +const SerializedModulesResult = struct { + /// Serialized bytes (owned by arena allocator) + bytes: []align(16) u8, + /// Entry point definition indices + entry_def_indices: []const u32, + /// Number of compilation errors encountered + error_count: usize, +}; + +/// Compile a single module to a ModuleEnv using a regular allocator. +/// Unlike compileModuleToSharedMemory, this uses the gpa and keeps source separate. +/// +/// exposed_type_module_names: Optional list of module names that are "type modules" (e.g., "Stdout", "Stderr"). +/// When provided, modules in additional_modules whose names match these will have their +/// statement_idx set correctly, enabling proper function lookup (e.g., Stdout.line!). +/// The order must match: exposed_type_module_names[i] corresponds to additional_modules[i]. +fn compileModuleForSerialization( + ctx: *CliContext, + file_path: []const u8, + module_name_arg: []const u8, + builtin_modules: *eval.BuiltinModules, + additional_modules: []*ModuleEnv, + exposed_type_module_names: ?[]const []const u8, +) !CompiledModule { + // Read file into arena (so it lives until serialization) + const file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) { + error.FileNotFound => return ctx.fail(.{ .file_not_found = .{ .path = file_path } }), + else => return ctx.fail(.{ .file_read_failed = .{ .path = file_path, .err = err } }), + }; + defer file.close(); + + const file_size = file.getEndPos() catch |err| { + return ctx.fail(.{ .file_read_failed = .{ .path = file_path, .err = err } }); + }; + var source = try ctx.arena.alloc(u8, @intCast(file_size)); + _ = file.read(source) catch |err| { + return ctx.fail(.{ .file_read_failed = .{ .path = file_path, .err = err } }); + }; + // Normalize line endings (CRLF -> LF) for consistent cross-platform parsing. + // The arena keeps the original allocation; trailing bytes (if any) are harmless. + source = base.source_utils.normalizeLineEndings(source); + + const module_name_copy = try ctx.arena.dupe(u8, module_name_arg); + + // Initialize ModuleEnv with gpa + var env = try ModuleEnv.init(ctx.gpa, source); + env.common.source = source; + env.module_name = module_name_copy; + try env.common.calcLineStarts(ctx.gpa); + + // Parse + var parse_ast = try parse.parse(&env.common, ctx.gpa); + defer parse_ast.deinit(ctx.gpa); + parse_ast.store.emptyScratch(); + + // Initialize CIR + try env.initCIRFields(module_name_copy); + + // Create module_envs map + var module_envs_map = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(ctx.gpa); + defer module_envs_map.deinit(); + + try Can.populateModuleEnvs( + &module_envs_map, + &env, + builtin_modules.builtin_module.env, + builtin_modules.builtin_indices, + ); + + // Extract platform qualifier from app header (e.g., "pf" from { pf: platform "..." }) + // This is needed to register modules with both base name and qualified name + const platform_qualifier: ?[]const u8 = blk: { + const parsed_file = parse_ast.store.getFile(); + const header = parse_ast.store.getHeader(parsed_file.header); + if (header == .app) { + const platform_field = parse_ast.store.getRecordField(header.app.platform_idx); + const key_region = parse_ast.tokens.resolve(platform_field.name); + break :blk source[key_region.start.offset..key_region.end.offset]; + } + break :blk null; + }; + + for (additional_modules, 0..) |mod_env, mod_idx| { + // Get the base module name (without .roc extension if present) + var base_module_name = mod_env.module_name; + if (std.mem.endsWith(u8, base_module_name, ".roc")) { + base_module_name = base_module_name[0 .. base_module_name.len - 4]; + } + + // Check if this module is a "type module" (platform module like Stdout that exposes a type). + // For type modules, we need to set statement_idx to enable proper function lookup. + const is_type_module = if (exposed_type_module_names) |type_names| + mod_idx < type_names.len and std.mem.eql(u8, type_names[mod_idx], base_module_name) + else + false; + + // Build the AutoImportedType entry + const auto_type: Can.AutoImportedType = if (is_type_module) blk: { + // For type modules, look up the type's node index + const type_qualified_ident = try env.insertIdent(base.Ident.for_text(base_module_name)); + const type_ident_in_module = mod_env.common.findIdent(base_module_name) orelse { + return ctx.fail(.{ .missing_type_in_module = .{ + .module_name = mod_env.module_name, + .type_name = base_module_name, + } }); + }; + const type_node_idx = mod_env.getExposedNodeIndexById(type_ident_in_module) orelse { + return ctx.fail(.{ .missing_type_in_module = .{ + .module_name = mod_env.module_name, + .type_name = base_module_name, + } }); + }; + break :blk .{ + .env = mod_env, + .statement_idx = @enumFromInt(type_node_idx), + .qualified_type_ident = type_qualified_ident, + }; + } else blk: { + // For regular modules (like platform main.roc), no statement_idx needed + const qualified_ident = try mod_env.common.insertIdent(mod_env.gpa, base.Ident.for_text(mod_env.module_name)); + break :blk .{ + .env = mod_env, + .statement_idx = null, + .qualified_type_ident = qualified_ident, + }; + }; + + // Register with base module name (e.g., "Stdout") + const name = try env.insertIdent(base.Ident.for_text(base_module_name)); + try module_envs_map.put(name, auto_type); + + // Register with full module name if different (e.g., "Stdout.roc") + if (!std.mem.eql(u8, mod_env.module_name, base_module_name)) { + const full_name = try env.insertIdent(base.Ident.for_text(mod_env.module_name)); + try module_envs_map.put(full_name, auto_type); + } + + // Register with platform-qualified name (e.g., "pf.Stdout" for apps) + if (platform_qualifier) |pf| { + const qualified_name = try std.fmt.allocPrint(ctx.gpa, "{s}.{s}", .{ pf, base_module_name }); + defer ctx.gpa.free(qualified_name); + const pf_name = try env.insertIdent(base.Ident.for_text(qualified_name)); + try module_envs_map.put(pf_name, auto_type); + } + } + + // Canonicalize + var canonicalizer = try Can.init(&env, &parse_ast, &module_envs_map); + defer canonicalizer.deinit(); + + try canonicalizer.canonicalizeFile(); + + // Run HostedCompiler to convert e_anno_only to e_hosted_lambda + const HostedCompiler = can.HostedCompiler; + var modified_def_indices = try HostedCompiler.replaceAnnoOnlyWithHosted(&env); + defer modified_def_indices.deinit(ctx.gpa); + + // Type check + var check_module_envs_map = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(ctx.gpa); + defer check_module_envs_map.deinit(); + + const builtin_ctx: Check.BuiltinContext = .{ + .module_name = try env.insertIdent(base.Ident.for_text(module_name_arg)), + .bool_stmt = builtin_modules.builtin_indices.bool_type, + .try_stmt = builtin_modules.builtin_indices.try_type, + .str_stmt = builtin_modules.builtin_indices.str_type, + .builtin_module = builtin_modules.builtin_module.env, + .builtin_indices = builtin_modules.builtin_indices, + }; + + // Build imported_envs array: builtins + additional modules + // This is needed for resolveImports to properly map external lookups + var imported_envs_list = try std.ArrayList(*const ModuleEnv).initCapacity(ctx.gpa, 1 + additional_modules.len); + defer imported_envs_list.deinit(ctx.gpa); + imported_envs_list.appendAssumeCapacity(builtin_modules.builtin_module.env); + for (additional_modules) |mod| { + imported_envs_list.appendAssumeCapacity(mod); + } + const imported_envs = imported_envs_list.items; + + env.imports.resolveImports(&env, imported_envs); + + var checker = try Check.init(ctx.gpa, &env.types, &env, imported_envs, &check_module_envs_map, &env.store.regions, builtin_ctx); + defer checker.deinit(); + + try checker.checkFile(); + + // Count and render errors from parsing, canonicalization, and type checking + var error_count: usize = 0; + var warning_count: usize = 0; + + const stderr = ctx.io.stderr(); + + // Render parse errors (tokenize and parse diagnostics) + if (parse_ast.hasErrors()) { + for (parse_ast.tokenize_diagnostics.items) |diagnostic| { + error_count += 1; + var report = parse_ast.tokenizeDiagnosticToReport(diagnostic, ctx.gpa, file_path) catch continue; + defer report.deinit(); + reporting.renderReportToTerminal(&report, stderr, ColorPalette.ANSI, reporting.ReportingConfig.initColorTerminal()) catch continue; + } + for (parse_ast.parse_diagnostics.items) |diagnostic| { + error_count += 1; + var report = parse_ast.parseDiagnosticToReport(&env.common, diagnostic, ctx.gpa, file_path) catch continue; + defer report.deinit(); + reporting.renderReportToTerminal(&report, stderr, ColorPalette.ANSI, reporting.ReportingConfig.initColorTerminal()) catch continue; + } + } + + // Render canonicalization diagnostics (unused variables, etc.) + const diags = env.getDiagnostics() catch &.{}; + defer env.gpa.free(diags); + for (diags) |d| { + var report = env.diagnosticToReport(d, env.gpa, file_path) catch continue; + defer report.deinit(); + reporting.renderReportToTerminal(&report, stderr, ColorPalette.ANSI, reporting.ReportingConfig.initColorTerminal()) catch continue; + if (report.severity == .fatal or report.severity == .runtime_error) { + error_count += 1; + } else if (report.severity == .warning) { + warning_count += 1; + } + } + + // Render type checking problems + var rb = ReportBuilder.init( + ctx.gpa, + &env, + &env, + &checker.snapshots, + file_path, + &.{}, + &checker.import_mapping, + ); + defer rb.deinit(); + + for (checker.problems.problems.items) |prob| { + var report = rb.build(prob) catch continue; + defer report.deinit(); + reporting.renderReportToTerminal(&report, stderr, ColorPalette.ANSI, reporting.ReportingConfig.initColorTerminal()) catch continue; + if (report.severity == .fatal or report.severity == .runtime_error) { + error_count += 1; + } else if (report.severity == .warning) { + warning_count += 1; + } + } + + // Print summary if there were any problems + if (error_count > 0 or warning_count > 0) { + stderr.writeAll("\n") catch {}; + stderr.print("Found {} error(s) and {} warning(s) for {s}.\n", .{ + error_count, + warning_count, + file_path, + }) catch {}; + } + + // Flush stderr to ensure all error output is visible + // Note: ctx.io.stderr() is typically unbuffered, so explicit flush is not needed + + return CompiledModule{ + .env = env, + .source = source, + .module_name = module_name_copy, + .is_platform_main = false, + .is_app = false, + .error_count = error_count, + }; +} + +/// Compile all modules and serialize them to a single buffer for embedding. +/// Returns the serialized bytes and entry point def indices. +fn compileAndSerializeModulesForEmbedding( + ctx: *CliContext, + roc_file_path: []const u8, + allow_errors: bool, +) !SerializedModulesResult { + // Track total errors across all modules + var total_error_count: usize = 0; + + // Load builtin modules + var builtin_modules = try eval.BuiltinModules.init(ctx.gpa); + defer builtin_modules.deinit(); + + const app_dir = std.fs.path.dirname(roc_file_path) orelse "."; + const platform_spec = try extractPlatformSpecFromApp(ctx, roc_file_path); + try validatePlatformSpec(ctx, platform_spec); + + // Resolve platform path + const platform_main_path: ?[]const u8 = if (std.mem.startsWith(u8, platform_spec, "./") or std.mem.startsWith(u8, platform_spec, "../")) + try std.fs.path.join(ctx.gpa, &[_][]const u8{ app_dir, platform_spec }) + else if (std.mem.startsWith(u8, platform_spec, "http://") or std.mem.startsWith(u8, platform_spec, "https://")) blk: { + const platform_paths = resolveUrlPlatform(ctx, platform_spec) catch |err| switch (err) { + error.CliError => break :blk null, + error.OutOfMemory => return error.OutOfMemory, + }; + break :blk platform_paths.platform_source_path; + } else null; + defer if (platform_main_path) |p| { + if (std.mem.startsWith(u8, platform_spec, "./") or std.mem.startsWith(u8, platform_spec, "../")) { + ctx.gpa.free(p); + } + }; + + const platform_dir: ?[]const u8 = if (platform_main_path) |p| + std.fs.path.dirname(p) orelse return error.InvalidPlatformPath + else + null; + + // Extract exposed modules from platform + var exposed_modules = std.ArrayList([]const u8).empty; + defer exposed_modules.deinit(ctx.gpa); + + var has_platform = false; + if (platform_main_path) |pmp| { + has_platform = true; + extractExposedModulesFromPlatform(ctx, pmp, &exposed_modules) catch { + has_platform = false; + }; + } + + // Compile all modules + var compiled_modules = std.array_list.Managed(CompiledModule).init(ctx.gpa); + defer { + for (compiled_modules.items) |*m| { + m.env.deinit(); + } + compiled_modules.deinit(); + } + + // Track indices + var primary_env_index: u32 = 0; + var app_env_index: u32 = 0; + + // Compile platform sibling modules first + // We need to track pointers to already-compiled modules so later modules can import from earlier ones. + // + // Modules are automatically sorted by their import dependencies using topological sort. + // If module A imports module B, B will be compiled before A regardless of the order + // in the platform's exposes list. + // + // Pre-allocate compiled_modules to avoid reallocation invalidating pointers in sibling_env_ptrs. + // platform_dir is guaranteed to be non-null if exposed_modules is non-empty + const plat_dir = platform_dir orelse unreachable; + const sorted_modules = sortPlatformModulesByDependency( + ctx, + exposed_modules.items, + plat_dir, + ) catch |err| { + if (err == error.CyclicDependency) { + std.log.err("Circular dependency detected in platform modules", .{}); + } + return err; + }; + defer ctx.gpa.free(sorted_modules); + + try compiled_modules.ensureTotalCapacity(sorted_modules.len + 2); // +2 for platform main and app + + var sibling_env_ptrs = try ctx.gpa.alloc(*ModuleEnv, sorted_modules.len); + defer ctx.gpa.free(sibling_env_ptrs); + + if (comptime trace_modules) { + std.debug.print("[TRACE-MODULES] === Build Mode: Compiling Platform Modules ===\n", .{}); + } + + for (sorted_modules, 0..) |module_name, i| { + const module_filename = try std.fmt.allocPrint(ctx.gpa, "{s}.roc", .{module_name}); + defer ctx.gpa.free(module_filename); + + const module_path = try std.fs.path.join(ctx.gpa, &[_][]const u8{ plat_dir, module_filename }); + defer ctx.gpa.free(module_path); + + if (comptime trace_modules) { + std.debug.print("[TRACE-MODULES] Compiling platform module {d}: \"{s}\" at {s}\n", .{ i, module_name, module_path }); + } + + // Pass previously compiled sibling modules so this module can resolve imports to them. + // This enables transitive module calls (e.g., Helper imports Core, then calls Core.wrap). + // Also pass sorted_modules[0..i] as type module names so opaque type methods can be resolved. + const compiled = try compileModuleForSerialization( + ctx, + module_path, + module_name, + &builtin_modules, + sibling_env_ptrs[0..i], + sorted_modules[0..i], // Pass type module names for previously compiled siblings + ); + total_error_count += compiled.error_count; + compiled_modules.appendAssumeCapacity(compiled); + // Store pointer to the env we just appended (safe because we pre-allocated) + sibling_env_ptrs[i] = &compiled_modules.items[compiled_modules.items.len - 1].env; + } + + // Compile platform main.roc if present + if (has_platform) { + if (comptime trace_modules) { + std.debug.print("[TRACE-MODULES] Compiling platform main: {s}\n", .{platform_main_path.?}); + } + + // Get pointers to already compiled platform modules + var platform_env_ptrs = try ctx.gpa.alloc(*ModuleEnv, compiled_modules.items.len); + defer ctx.gpa.free(platform_env_ptrs); + for (compiled_modules.items, 0..) |*m, i| { + platform_env_ptrs[i] = &m.env; + } + + var compiled = try compileModuleForSerialization( + ctx, + platform_main_path.?, + "main", + &builtin_modules, + platform_env_ptrs, + sorted_modules, // Pass type module names so platform main can resolve opaque type methods + ); + compiled.is_platform_main = true; + total_error_count += compiled.error_count; + primary_env_index = @intCast(compiled_modules.items.len); + try compiled_modules.append(compiled); + } + + // Compile app module + { + if (comptime trace_modules) { + std.debug.print("[TRACE-MODULES] Compiling app: {s}\n", .{roc_file_path}); + } + + var all_env_ptrs = try ctx.gpa.alloc(*ModuleEnv, compiled_modules.items.len); + defer ctx.gpa.free(all_env_ptrs); + for (compiled_modules.items, 0..) |*m, i| { + all_env_ptrs[i] = &m.env; + } + + var compiled = try compileModuleForSerialization( + ctx, + roc_file_path, + "app", + &builtin_modules, + all_env_ptrs, + sorted_modules, // Pass type module names in same order as all_env_ptrs + ); + compiled.is_app = true; + total_error_count += compiled.error_count; + app_env_index = @intCast(compiled_modules.items.len); + if (!has_platform) { + primary_env_index = app_env_index; + } + try compiled_modules.append(compiled); + } + + // Collect and sort all hosted functions globally, then assign indices + // This must happen before serialization so hosted_idx values are correct + { + const HostedCompiler = can.HostedCompiler; + var all_hosted_fns = std.ArrayList(HostedCompiler.HostedFunctionInfo).empty; + defer all_hosted_fns.deinit(ctx.gpa); + + // Collect from platform sibling modules only (not app, not platform main.roc) + for (compiled_modules.items, 0..) |*m, i| { + // Skip app module and platform main.roc + if (i == app_env_index or i == primary_env_index) continue; + var module_fns = try HostedCompiler.collectAndSortHostedFunctions(&m.env); + defer module_fns.deinit(m.env.gpa); + + for (module_fns.items) |fn_info| { + try all_hosted_fns.append(ctx.gpa, fn_info); + } + } + + // Sort globally + const SortContext = struct { + pub fn lessThan(_: void, a: HostedCompiler.HostedFunctionInfo, b: HostedCompiler.HostedFunctionInfo) bool { + return std.mem.order(u8, a.name_text, b.name_text) == .lt; + } + }; + std.mem.sort(HostedCompiler.HostedFunctionInfo, all_hosted_fns.items, {}, SortContext.lessThan); + + // Deduplicate + var write_idx: usize = 0; + for (all_hosted_fns.items, 0..) |fn_info, read_idx| { + if (write_idx == 0 or !std.mem.eql(u8, all_hosted_fns.items[write_idx - 1].name_text, fn_info.name_text)) { + if (write_idx != read_idx) { + all_hosted_fns.items[write_idx] = fn_info; + } + write_idx += 1; + } else { + ctx.gpa.free(fn_info.name_text); + } + } + all_hosted_fns.shrinkRetainingCapacity(write_idx); + + // Reassign global indices for platform sibling modules only + // (not app, not platform main.roc - only exposed modules like Stdout, Stderr, Stdin) + for (compiled_modules.items, 0..) |*m, module_idx| { + // Skip app module and platform main.roc + if (module_idx == app_env_index or module_idx == primary_env_index) continue; + const platform_env = &m.env; + + const all_defs = platform_env.store.sliceDefs(platform_env.all_defs); + for (all_defs) |def_idx| { + const def = platform_env.store.getDef(def_idx); + const expr = platform_env.store.getExpr(def.expr); + + if (expr == .e_hosted_lambda) { + const hosted = expr.e_hosted_lambda; + const local_name = platform_env.getIdent(hosted.symbol_name); + + var plat_module_name = platform_env.module_name; + if (std.mem.endsWith(u8, plat_module_name, ".roc")) { + plat_module_name = plat_module_name[0 .. plat_module_name.len - 4]; + } + const qualified_name = try std.fmt.allocPrint(ctx.gpa, "{s}.{s}", .{ plat_module_name, local_name }); + defer ctx.gpa.free(qualified_name); + + const stripped_name = if (std.mem.endsWith(u8, qualified_name, "!")) + qualified_name[0 .. qualified_name.len - 1] + else + qualified_name; + + for (all_hosted_fns.items, 0..) |fn_info, idx| { + if (std.mem.eql(u8, fn_info.name_text, stripped_name)) { + const expr_node_idx = @as(@TypeOf(platform_env.store.nodes).Idx, @enumFromInt(@intFromEnum(def.expr))); + var expr_node = platform_env.store.nodes.get(expr_node_idx); + expr_node.data_2 = @intCast(idx); + platform_env.store.nodes.set(expr_node_idx, expr_node); + break; + } + } + } + } + } + + // Free name_text strings + for (all_hosted_fns.items) |fn_info| { + ctx.gpa.free(fn_info.name_text); + } + } + + // Check for errors - abort unless --allow-errors flag is set + if (total_error_count > 0 and !allow_errors) { + return error.CompilationErrors; + } + + // Get entry points from primary environment + // Use exports (not all_defs) to only include exported definitions as entry points. + // all_defs includes method definitions from associated blocks which should not be entry points. + const primary_env = &compiled_modules.items[primary_env_index].env; + const entry_defs = primary_env.exports; + const entry_count: u32 = entry_defs.span.len; + + // Build entry def indices - use sliceDefs to get actual Def.Idx values + // (all_defs.span indexes into extra_data which contains Def.Idx values) + const entry_def_indices = try ctx.arena.alloc(u32, entry_count); + const defs_slice = primary_env.store.sliceDefs(entry_defs); + for (defs_slice, 0..) |def_idx, i| { + entry_def_indices[i] = @intFromEnum(def_idx); + } + + // Now serialize everything using CompactWriter + var writer = CompactWriter.init(); + defer writer.deinit(ctx.gpa); + + const module_count: u32 = @intCast(compiled_modules.items.len); + + // 1. Allocate and fill header + const header = try writer.appendAlloc(ctx.gpa, SerializedHeader); + header.magic = SERIALIZED_FORMAT_MAGIC; + header.format_version = 1; + header.module_count = module_count; + header.entry_count = entry_count; + header.primary_env_index = primary_env_index; + header.app_env_index = app_env_index; + // def_indices_offset and module_infos_offset will be set later + + // 2. Allocate module info array + try writer.padToAlignment(ctx.gpa, @alignOf(SerializedModuleInfo)); + header.module_infos_offset = writer.total_bytes; + const module_infos = try ctx.gpa.alloc(SerializedModuleInfo, module_count); + defer ctx.gpa.free(module_infos); + + // Add module infos to writer (we'll fill in offsets as we serialize) + try writer.iovecs.append(ctx.gpa, .{ + .iov_base = @ptrCast(module_infos.ptr), + .iov_len = module_count * @sizeOf(SerializedModuleInfo), + }); + writer.total_bytes += module_count * @sizeOf(SerializedModuleInfo); + + // 3. Serialize source bytes and module names for each module + for (compiled_modules.items, 0..) |*m, i| { + // Source bytes + try writer.padToAlignment(ctx.gpa, 1); + module_infos[i].source_offset = writer.total_bytes; + module_infos[i].source_len = m.source.len; + if (m.source.len > 0) { + try writer.iovecs.append(ctx.gpa, .{ + .iov_base = m.source.ptr, + .iov_len = m.source.len, + }); + writer.total_bytes += m.source.len; + } + + // Module name + try writer.padToAlignment(ctx.gpa, 1); + module_infos[i].module_name_offset = writer.total_bytes; + module_infos[i].module_name_len = m.module_name.len; + if (m.module_name.len > 0) { + try writer.iovecs.append(ctx.gpa, .{ + .iov_base = m.module_name.ptr, + .iov_len = m.module_name.len, + }); + writer.total_bytes += m.module_name.len; + } + } + + // 4. Serialize each ModuleEnv + for (compiled_modules.items, 0..) |*m, i| { + // Ensure 8-byte alignment for ModuleEnv.Serialized (it contains u64/i64 fields) + // This is critical for cross-architecture builds (e.g., wasm32) + try writer.padToAlignment(ctx.gpa, 8); + + // Record the offset before allocating - this is where the serialized env will be + const env_offset_before = writer.total_bytes; + const serialized_env = try writer.appendAlloc(ctx.gpa, ModuleEnv.Serialized); + module_infos[i].env_serialized_offset = env_offset_before; + + try serialized_env.serialize(&m.env, ctx.gpa, &writer); + } + + // 5. Serialize entry point def indices + try writer.padToAlignment(ctx.gpa, @alignOf(u32)); + header.def_indices_offset = writer.total_bytes; + if (entry_count > 0) { + try writer.iovecs.append(ctx.gpa, .{ + .iov_base = @ptrCast(entry_def_indices.ptr), + .iov_len = entry_count * @sizeOf(u32), + }); + writer.total_bytes += entry_count * @sizeOf(u32); + } + + // 6. Write all to buffer + const buffer = try ctx.arena.alignedAlloc(u8, CompactWriter.SERIALIZATION_ALIGNMENT, writer.total_bytes); + _ = try writer.writeToBuffer(buffer); + + return SerializedModulesResult{ + .bytes = buffer, + .entry_def_indices = entry_def_indices, + .error_count = total_error_count, }; } @@ -907,10 +3259,12 @@ fn writeToPosixSharedMemory(data: []const u8, total_size: usize) !SharedMemoryHa 0x0001, // MAP_SHARED shm_fd, 0, - ) orelse { + ); + // mmap returns MAP_FAILED ((void*)-1) on error, not NULL + if (mapped_ptr == posix.MAP_FAILED) { _ = c.close(shm_fd); return error.SharedMemoryMapFailed; - }; + } const mapped_memory = @as([*]u8, @ptrCast(mapped_ptr))[0..total_size]; // Write length at the beginning @@ -928,128 +3282,389 @@ fn writeToPosixSharedMemory(data: []const u8, total_size: usize) !SharedMemoryHa }; } -/// Resolve platform specification from a Roc file to find the host library -pub fn resolvePlatformHost(gpa: std.mem.Allocator, roc_file_path: []const u8) (std.mem.Allocator.Error || error{ NoPlatformFound, PlatformNotSupported })![]u8 { - // Read the Roc file to parse the app header - const roc_file = std.fs.cwd().openFile(roc_file_path, .{}) catch |err| switch (err) { - error.FileNotFound => return error.NoPlatformFound, - else => return error.NoPlatformFound, // Treat all file errors as no platform found +/// Platform resolution result containing the platform source path +pub const PlatformPaths = struct { + platform_source_path: ?[]const u8, // Optional - may not exist for some platforms +}; + +/// Resolve platform specification from a Roc file to find both host library and platform source. +/// Returns PlatformPaths with arena-allocated paths (no need to free). +pub fn resolvePlatformPaths(ctx: *CliContext, roc_file_path: []const u8) CliError!PlatformPaths { + // Use the parser to extract the platform spec + const platform_spec = extractPlatformSpecFromApp(ctx, roc_file_path) catch { + return ctx.fail(.{ .file_not_found = .{ + .path = roc_file_path, + .context = .source_file, + } }); }; - defer roc_file.close(); + const app_dir = std.fs.path.dirname(roc_file_path) orelse "."; + return resolvePlatformSpecToPaths(ctx, platform_spec, app_dir); +} - const file_size = roc_file.getEndPos() catch return error.NoPlatformFound; - const source = gpa.alloc(u8, @intCast(file_size)) catch return error.OutOfMemory; - defer gpa.free(source); - _ = roc_file.read(source) catch return error.NoPlatformFound; +/// Extract platform specification from app file header by parsing it properly. +/// Takes a CliContext which provides allocators and error reporting. +fn extractPlatformSpecFromApp(ctx: *CliContext, app_file_path: []const u8) ![]const u8 { + // Read the app file + var source = std.fs.cwd().readFileAlloc(ctx.gpa, app_file_path, std.math.maxInt(usize)) catch |err| { + return ctx.fail(switch (err) { + error.FileNotFound => .{ .file_not_found = .{ + .path = app_file_path, + .context = .source_file, + } }, + else => .{ .file_read_failed = .{ + .path = app_file_path, + .err = err, + } }, + }); + }; + source = base.source_utils.normalizeLineEndingsRealloc(ctx.gpa, source) catch |err| { + ctx.gpa.free(source); + return err; + }; + defer ctx.gpa.free(source); - // Parse the source to find the app header - // Look for "app" followed by platform specification - var lines = std.mem.splitScalar(u8, source, '\n'); - while (lines.next()) |line| { - const trimmed = std.mem.trim(u8, line, " \t\r"); + // Extract module name from file path + const basename = std.fs.path.basename(app_file_path); + const module_name = try ctx.arena.dupe(u8, basename); - // Check if this is an app header line - if (std.mem.startsWith(u8, trimmed, "app")) { - // Look for platform specification after "platform" - if (std.mem.indexOf(u8, trimmed, "platform")) |platform_start| { - const after_platform = trimmed[platform_start + "platform".len ..]; + // Create ModuleEnv for parsing + var env = ModuleEnv.init(ctx.gpa, source) catch { + return ctx.fail(.{ .module_init_failed = .{ + .path = app_file_path, + .err = error.OutOfMemory, + } }); + }; + defer env.deinit(); - // Find the platform name/URL in quotes - if (std.mem.indexOf(u8, after_platform, "\"")) |quote_start| { - const after_quote = after_platform[quote_start + 1 ..]; - if (std.mem.indexOf(u8, after_quote, "\"")) |quote_end| { - const platform_spec = after_quote[0..quote_end]; + env.common.source = source; + env.module_name = module_name; + env.common.calcLineStarts(ctx.gpa) catch { + return ctx.fail(.{ .module_init_failed = .{ + .path = app_file_path, + .err = error.OutOfMemory, + } }); + }; - // If it's a relative path, resolve it relative to the app directory - if (std.mem.startsWith(u8, platform_spec, "./") or std.mem.startsWith(u8, platform_spec, "../")) { - const app_dir = std.fs.path.dirname(roc_file_path) orelse "."; - const platform_path = try std.fs.path.join(gpa, &.{ app_dir, platform_spec }); - defer gpa.free(platform_path); + // Parse the source + var ast = parse.parse(&env.common, ctx.gpa) catch { + return ctx.fail(.{ .module_init_failed = .{ + .path = app_file_path, + .err = error.OutOfMemory, + } }); + }; + defer ast.deinit(ctx.gpa); - // Look for host library near the platform file - const platform_dir = std.fs.path.dirname(platform_path) orelse "."; - const host_filename = if (comptime builtin.target.os.tag == .windows) "host.lib" else "libhost.a"; - const host_path = try std.fs.path.join(gpa, &.{ platform_dir, host_filename }); - defer gpa.free(host_path); + // Get the file header + const file = ast.store.getFile(); + const header = ast.store.getHeader(file.header); - std.fs.cwd().access(host_path, .{}) catch { - return error.PlatformNotSupported; - }; + // Check if this is an app file + switch (header) { + .app => |a| { + // Get the platform field + const pf = ast.store.getRecordField(a.platform_idx); + const value_expr = pf.value orelse { + return ctx.fail(.{ .expected_platform_string = .{ .path = app_file_path } }); + }; - return try gpa.dupe(u8, host_path); - } + // Extract the string value from the expression + const platform_spec = stringFromExpr(&ast, value_expr) catch { + return ctx.fail(.{ .expected_platform_string = .{ .path = app_file_path } }); + }; + return try ctx.arena.dupe(u8, platform_spec); + }, + else => { + return ctx.fail(.{ .expected_app_header = .{ + .path = app_file_path, + .found = @tagName(header), + } }); + }, + } +} - // Try to resolve platform to a local host library - return resolvePlatformSpecToHostLib(gpa, platform_spec); - } +/// Extract a string value from an expression (for platform/package paths). +fn stringFromExpr(ast: *parse.AST, expr_idx: parse.AST.Expr.Idx) ![]const u8 { + const e = ast.store.getExpr(expr_idx); + return switch (e) { + .string => |s| { + // For simple strings, iterate through the parts + for (ast.store.exprSlice(s.parts)) |part_idx| { + const part = ast.store.getExpr(part_idx); + if (part == .string_part) { + // Return the first string part (platform specs are simple strings) + return ast.resolve(part.string_part.token); } } - } - } - - return error.NoPlatformFound; + return error.ExpectedString; + }, + else => error.ExpectedString, + }; } -/// Resolve a platform specification to a local host library path -fn resolvePlatformSpecToHostLib(gpa: std.mem.Allocator, platform_spec: []const u8) (std.mem.Allocator.Error || error{PlatformNotSupported})![]u8 { +/// Check if platform spec is an absolute path and reject it. +/// Uses CliContext for error reporting. +fn validatePlatformSpec(ctx: *CliContext, platform_spec: []const u8) CliError!void { + if (std.fs.path.isAbsolute(platform_spec)) { + return ctx.fail(.{ .absolute_platform_path = .{ .platform_spec = platform_spec } }); + } +} - // Check for common platform names and map them to host libraries - if (std.mem.eql(u8, platform_spec, "cli")) { - // Try to find CLI platform host library - const cli_paths = if (comptime builtin.target.os.tag == .windows) - [_][]const u8{ - "zig-out/lib/platform_host_cli.lib", - "platform/cli/host.lib", - "platforms/cli/host.lib", - } - else - [_][]const u8{ - "zig-out/lib/libplatform_host_cli.a", - "platform/cli/host.a", - "platforms/cli/host.a", - }; - - for (cli_paths) |path| { - std.fs.cwd().access(path, .{}) catch continue; - return try gpa.dupe(u8, path); - } - } else if (std.mem.eql(u8, platform_spec, "basic-cli")) { - // Try to find basic-cli platform host library - const basic_cli_paths = if (comptime builtin.target.os.tag == .windows) - [_][]const u8{ - "zig-out/lib/platform_host_basic_cli.lib", - "platform/basic-cli/host.lib", - "platforms/basic-cli/host.lib", - } - else - [_][]const u8{ - "zig-out/lib/libplatform_host_basic_cli.a", - "platform/basic-cli/host.a", - "platforms/basic-cli/host.a", - }; - - for (basic_cli_paths) |path| { - std.fs.cwd().access(path, .{}) catch continue; - return try gpa.dupe(u8, path); - } - } else if (std.mem.startsWith(u8, platform_spec, "http")) { - // This is a URL - for production, would download and resolve - // For now, return not supported - return error.PlatformNotSupported; +/// Resolve a platform specification to a platform source path. +/// Uses CliContext for error reporting. +fn resolvePlatformSpecToPaths(ctx: *CliContext, platform_spec: []const u8, base_dir: []const u8) CliError!PlatformPaths { + // Handle URL-based platforms + if (std.mem.startsWith(u8, platform_spec, "http")) { + return resolveUrlPlatform(ctx, platform_spec) catch |err| switch (err) { + error.CliError => return error.CliError, + error.OutOfMemory => return ctx.fail(.{ .cache_dir_unavailable = .{ + .reason = "Out of memory while resolving URL platform", + } }), + }; } - // Try to interpret as a direct file path - std.fs.cwd().access(platform_spec, .{}) catch { - return error.PlatformNotSupported; + // Check for absolute paths and reject them + try validatePlatformSpec(ctx, platform_spec); + + // Try to interpret as a file path (must be relative, resolve relative to base_dir) + const resolved_path = std.fs.path.join(ctx.arena, &.{ base_dir, platform_spec }) catch { + return ctx.fail(.{ .file_read_failed = .{ + .path = platform_spec, + .err = error.OutOfMemory, + } }); }; - return try gpa.dupe(u8, platform_spec); + std.fs.cwd().access(resolved_path, .{}) catch { + return ctx.fail(.{ .platform_not_found = .{ + .app_path = base_dir, + .platform_path = resolved_path, + } }); + }; + + // Platform spec should point to a .roc file + if (std.mem.endsWith(u8, resolved_path, ".roc")) { + return PlatformPaths{ + .platform_source_path = ctx.arena.dupe(u8, resolved_path) catch { + return ctx.fail(.{ .file_read_failed = .{ + .path = resolved_path, + .err = error.OutOfMemory, + } }); + }, + }; + } else { + // Non-.roc file path - not supported + return ctx.fail(.{ .platform_validation_failed = .{ + .message = "Platform path must end with .roc", + } }); + } } -/// Extract the embedded roc_shim library to the specified path -/// This library contains the shim code that runs in child processes to read ModuleEnv from shared memory -pub fn extractReadRocFilePathShimLibrary(gpa: Allocator, output_path: []const u8) !void { - _ = gpa; // unused but kept for consistency +/// Get the roc cache directory for downloaded packages, creating it if needed. +/// Standard cache locations by platform: +/// - Linux/macOS: ~/.cache/roc/packages/ (respects XDG_CACHE_HOME if set) +/// - Windows: %LOCALAPPDATA%\roc\packages\ +fn getRocCacheDir(allocator: std.mem.Allocator) ![]const u8 { + // Check XDG_CACHE_HOME first (Linux/macOS) + if (getEnvVar(allocator, "XDG_CACHE_HOME")) |xdg_cache| { + defer allocator.free(xdg_cache); + return std.fs.path.join(allocator, &.{ xdg_cache, "roc", "packages" }); + } + + // Fall back to %LOCALAPPDATA%\roc\packages (Windows) + if (comptime builtin.os.tag == .windows) { + if (getEnvVar(allocator, "LOCALAPPDATA")) |local_app_data| { + defer allocator.free(local_app_data); + return std.fs.path.join(allocator, &.{ local_app_data, "roc", "packages" }); + } + } + + // Fall back to ~/.cache/roc/packages (Unix) + if (getEnvVar(allocator, "HOME")) |home| { + defer allocator.free(home); + return std.fs.path.join(allocator, &.{ home, ".cache", "roc", "packages" }); + } + + return error.NoCacheDir; +} + +/// Cross-platform helper to get environment variable. +/// Returns null if the variable is not set. Caller must free the returned slice. +fn getEnvVar(allocator: std.mem.Allocator, key: []const u8) ?[]const u8 { + return std.process.getEnvVarOwned(allocator, key) catch null; +} + +/// Resolve a URL platform specification by downloading and caching the bundle. +/// The URL must point to a .tar.zst bundle with a base58-encoded BLAKE3 hash filename. +fn resolveUrlPlatform(ctx: *CliContext, url: []const u8) (CliError || error{OutOfMemory})!PlatformPaths { + const download = unbundle.download; + + // 1. Validate URL and extract hash + const base58_hash = download.validateUrl(url) catch { + return ctx.fail(.{ .invalid_url = .{ + .url = url, + .reason = "Invalid platform URL format or missing hash", + } }); + }; + + // 2. Get cache directory + const cache_dir_path = getRocCacheDir(ctx.arena) catch { + return ctx.fail(.{ .cache_dir_unavailable = .{ .reason = "Could not determine cache directory" } }); + }; + const package_dir_path = try std.fs.path.join(ctx.arena, &.{ cache_dir_path, base58_hash }); + + // 3. Check if already cached + var package_dir = std.fs.cwd().openDir(package_dir_path, .{}) catch |err| switch (err) { + error.FileNotFound => blk: { + // Not cached - need to download + std.log.info("Downloading platform from {s}...", .{url}); + + // Create cache directory structure + std.fs.cwd().makePath(cache_dir_path) catch |make_err| { + return ctx.fail(.{ .directory_create_failed = .{ + .path = cache_dir_path, + .err = make_err, + } }); + }; + + // Create package directory + std.fs.cwd().makeDir(package_dir_path) catch |make_err| switch (make_err) { + error.PathAlreadyExists => {}, // Race condition, another process created it + else => { + return ctx.fail(.{ .directory_create_failed = .{ + .path = package_dir_path, + .err = make_err, + } }); + }, + }; + + var new_package_dir = std.fs.cwd().openDir(package_dir_path, .{}) catch { + return ctx.fail(.{ .directory_not_found = .{ + .path = package_dir_path, + } }); + }; + + // Download and extract + var gpa_copy = ctx.gpa; + download.downloadAndExtract(&gpa_copy, url, new_package_dir) catch |download_err| { + // Clean up failed download + new_package_dir.close(); + std.fs.cwd().deleteTree(package_dir_path) catch {}; + return ctx.fail(.{ .download_failed = .{ + .url = url, + .err = download_err, + } }); + }; + + std.log.info("Platform cached at {s}", .{package_dir_path}); + break :blk new_package_dir; + }, + else => { + return ctx.fail(.{ .directory_not_found = .{ + .path = package_dir_path, + } }); + }, + }; + defer package_dir.close(); + + // Platforms must have a main.roc entry point + const platform_source_path = try std.fs.path.join(ctx.arena, &.{ package_dir_path, "main.roc" }); + std.fs.cwd().access(platform_source_path, .{}) catch { + return ctx.fail(.{ .platform_source_not_found = .{ + .platform_path = package_dir_path, + .searched_paths = &.{platform_source_path}, + } }); + }; + + return PlatformPaths{ + .platform_source_path = platform_source_path, + }; +} + +/// Extract all entrypoint names from platform header provides record into ArrayList +/// TODO: Replace this with proper BuildEnv solution in the future +fn extractEntrypointsFromPlatform(ctx: *CliContext, roc_file_path: []const u8, entrypoints: *std.array_list.Managed([]const u8)) !void { + // Read the Roc file + var source = std.fs.cwd().readFileAlloc(ctx.gpa, roc_file_path, std.math.maxInt(usize)) catch return error.NoPlatformFound; + source = base.source_utils.normalizeLineEndingsRealloc(ctx.gpa, source) catch |err| { + ctx.gpa.free(source); + return err; + }; + defer ctx.gpa.free(source); + + // Extract module name from the file path + const basename = std.fs.path.basename(roc_file_path); + const module_name = try ctx.arena.dupe(u8, basename); + + // Create ModuleEnv + var env = ModuleEnv.init(ctx.gpa, source) catch return error.ParseFailed; + defer env.deinit(); + + env.common.source = source; + env.module_name = module_name; + try env.common.calcLineStarts(ctx.gpa); + + // Parse the source code as a full module + var parse_ast = parse.parse(&env.common, ctx.gpa) catch return error.ParseFailed; + defer parse_ast.deinit(ctx.gpa); + + // Look for platform header in the AST + const file_node = parse_ast.store.getFile(); + const header = parse_ast.store.getHeader(file_node.header); + + // Check if this is a platform file with a platform header + switch (header) { + .platform => |platform_header| { + // Get the provides collection and its record fields + const provides_coll = parse_ast.store.getCollection(platform_header.provides); + const provides_fields = parse_ast.store.recordFieldSlice(.{ .span = provides_coll.span }); + + // Extract FFI symbol names from provides clause + // Format: `provides { roc_identifier: "ffi_symbol_name" }` + // The string value specifies the symbol name exported to the host (becomes roc__) + for (provides_fields) |field_idx| { + const field = parse_ast.store.getRecordField(field_idx); + + // Require explicit string value for symbol name + const symbol_name = if (field.value) |value_idx| blk: { + const value_expr = parse_ast.store.getExpr(value_idx); + switch (value_expr) { + .string => |str_like| { + const parts = parse_ast.store.exprSlice(str_like.parts); + if (parts.len > 0) { + const first_part = parse_ast.store.getExpr(parts[0]); + switch (first_part) { + .string_part => |sp| break :blk parse_ast.resolve(sp.token), + else => {}, + } + } + return error.InvalidProvidesEntry; + }, + .string_part => |str_part| break :blk parse_ast.resolve(str_part.token), + else => { + return error.InvalidProvidesEntry; + }, + } + } else { + return error.InvalidProvidesEntry; + }; + try entrypoints.append(try ctx.arena.dupe(u8, symbol_name)); + } + + if (provides_fields.len == 0) { + return error.NoEntrypointFound; + } + }, + else => { + return error.NotPlatformFile; + }, + } +} + +/// Extract the embedded roc_shim library to the specified path for the given target. +/// This library contains the shim code that runs in child processes to read ModuleEnv from shared memory. +/// For native builds and roc run, use the native shim (pass null or native target). +/// For cross-compilation, pass the target to get the appropriate shim. +pub fn extractReadRocFilePathShimLibrary(ctx: *CliContext, output_path: []const u8, target: ?roc_target.RocTarget) !void { + _ = ctx; // unused but kept for consistency if (builtin.is_test) { // In test mode, create an empty file to avoid embedding issues @@ -1058,11 +3673,17 @@ pub fn extractReadRocFilePathShimLibrary(gpa: Allocator, output_path: []const u8 return; } + // Get the appropriate shim for the target (or native if not specified) + const shim_data = if (target) |t| + ShimLibraries.forTarget(t) + else + ShimLibraries.native; + // Write the embedded shim library to the output path const shim_file = try std.fs.cwd().createFile(output_path, .{}); defer shim_file.close(); - try shim_file.writeAll(roc_shim_lib); + try shim_file.writeAll(shim_data); } /// Format a bundle path validation reason into a user-friendly error message @@ -1120,14 +3741,9 @@ fn formatUnbundlePathValidationReason(reason: unbundle.PathValidationReason) []c } /// Bundles a roc package and its dependencies into a compressed tar archive -pub fn rocBundle(gpa: Allocator, args: cli_args.BundleArgs) !void { - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); - - // Use arena allocator for all bundle operations - var arena = std.heap.ArenaAllocator.init(gpa); - defer arena.deinit(); - const arena_allocator = arena.allocator(); +pub fn rocBundle(ctx: *CliContext, args: cli_args.BundleArgs) !void { + const stdout = ctx.io.stdout(); + const stderr = ctx.io.stderr(); // Start timing const start_time = std.time.nanoTimestamp(); @@ -1150,8 +3766,8 @@ pub fn rocBundle(gpa: Allocator, args: cli_args.BundleArgs) !void { } // Collect all files to bundle - var file_paths = std.ArrayList([]const u8).init(arena_allocator); - defer file_paths.deinit(); + var file_paths = std.ArrayList([]const u8).empty; + defer file_paths.deinit(ctx.arena); var uncompressed_size: u64 = 0; @@ -1172,7 +3788,7 @@ pub fn rocBundle(gpa: Allocator, args: cli_args.BundleArgs) !void { const stat = try file.stat(); uncompressed_size += stat.size; - try file_paths.append(path); + try file_paths.append(ctx.arena, path); } // Sort and deduplicate paths @@ -1213,9 +3829,51 @@ pub fn rocBundle(gpa: Allocator, args: cli_args.BundleArgs) !void { } } + // Find the platform file among the .roc files (if any) + // We need to check each file without side effects first, then validate the actual platform + var platform_file: ?[]const u8 = null; + for (file_paths.items) |path| { + if (std.mem.endsWith(u8, path, ".roc")) { + if (platform_validation.isPlatformFile(ctx.arena, path)) |is_platform| { + if (is_platform) { + platform_file = path; + break; + } + } + } + } + + // If we found a platform file, validate it has proper targets section + if (platform_file) |pf| { + if (platform_validation.validatePlatformHeader(ctx.arena, pf)) |validation| { + // Platform validation succeeded - validate all target files exist + if (platform_validation.validateAllTargetFilesExist( + ctx.arena, + validation.config, + validation.platform_dir, + )) |result| { + // Render the validation error with nice formatting + _ = platform_validation.renderValidationError(ctx.gpa, result, stderr); + return switch (result) { + .missing_target_file => error.MissingTargetFile, + .missing_files_directory => error.MissingFilesDirectory, + else => error.MissingTargetFile, + }; + } + } else |_| { + // validatePlatformHeader already rendered the error message via the reporting system. + // We continue bundling for now (non-blocking warning), but the user has seen the error. + // This allows bundling apps or platforms that don't yet have targets sections. + } + } + // Create temporary output file const temp_filename = "temp_bundle.tar.zst"; - const temp_file = try tmp_dir.createFile(temp_filename, .{}); + const temp_file = try tmp_dir.createFile(temp_filename, .{ + // Allow querying metadata (stat) on the handle, necessary for windows + .read = true, + .truncate = true, + }); defer temp_file.close(); // Create file path iterator @@ -1234,13 +3892,15 @@ pub fn rocBundle(gpa: Allocator, args: cli_args.BundleArgs) !void { var iter = FilePathIterator{ .paths = file_paths.items }; // Bundle the files - var allocator_copy = arena_allocator; + var allocator_copy = ctx.arena; var error_ctx: bundle.ErrorContext = undefined; + var temp_writer_buffer: [4096]u8 = undefined; + var temp_writer = temp_file.writer(&temp_writer_buffer); const final_filename = bundle.bundleFiles( &iter, @intCast(args.compression_level), &allocator_copy, - temp_file.writer(), + &temp_writer.interface, cwd, null, // path_prefix parameter - null means no stripping &error_ctx, @@ -1253,6 +3913,8 @@ pub fn rocBundle(gpa: Allocator, args: cli_args.BundleArgs) !void { }; // No need to free when using arena allocator + try temp_writer.interface.flush(); + // Get the compressed file size const compressed_stat = try temp_file.stat(); const compressed_size = compressed_stat.size; @@ -1269,7 +3931,7 @@ pub fn rocBundle(gpa: Allocator, args: cli_args.BundleArgs) !void { const display_path = if (args.output_dir == null) final_filename else - try std.fs.path.join(arena_allocator, &.{ args.output_dir.?, final_filename }); + try std.fs.path.join(ctx.arena, &.{ args.output_dir.?, final_filename }); // No need to free when using arena allocator // Print results @@ -1280,9 +3942,9 @@ pub fn rocBundle(gpa: Allocator, args: cli_args.BundleArgs) !void { try stdout.print("Time: {} ms\n", .{elapsed_ms}); } -fn rocUnbundle(allocator: Allocator, args: cli_args.UnbundleArgs) !void { - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); +fn rocUnbundle(ctx: *CliContext, args: cli_args.UnbundleArgs) !void { + const stdout = ctx.io.stdout(); + const stderr = ctx.io.stderr(); const cwd = std.fs.cwd(); var had_errors = false; @@ -1330,9 +3992,11 @@ fn rocUnbundle(allocator: Allocator, args: cli_args.UnbundleArgs) !void { // Unbundle the archive var error_ctx: unbundle.ErrorContext = undefined; + var archive_reader_buffer: [4096]u8 = undefined; + var archive_reader = archive_file.reader(&archive_reader_buffer); unbundle.unbundleFiles( - allocator, - archive_file.reader(), + ctx.gpa, + &archive_reader.interface, output_dir, basename, &error_ctx, @@ -1364,419 +4028,820 @@ fn rocUnbundle(allocator: Allocator, args: cli_args.UnbundleArgs) !void { } if (had_errors) { - std.process.exit(1); + return error.UnbundleFailed; } } -fn rocBuild(gpa: Allocator, args: cli_args.BuildArgs) !void { +fn rocBuild(ctx: *CliContext, args: cli_args.BuildArgs) !void { // Handle the --z-bench-tokenize flag if (args.z_bench_tokenize) |file_path| { - try benchTokenizer(gpa, file_path); + try benchTokenizer(ctx.gpa, file_path); return; } // Handle the --z-bench-parse flag if (args.z_bench_parse) |directory_path| { - try benchParse(gpa, directory_path); + try benchParse(ctx.gpa, directory_path); return; } - fatal("build not implemented", .{}); + // Use embedded interpreter build approach + // This compiles the Roc app, serializes the ModuleEnv, and embeds it in the binary + try rocBuildEmbedded(ctx, args); } +/// Build a standalone binary with the interpreter and embedded module data. +/// This is the primary build path that creates executables or libraries without requiring IPC. +fn rocBuildEmbedded(ctx: *CliContext, args: cli_args.BuildArgs) !void { + const target_mod = @import("target.zig"); + + std.log.info("Building {s} with embedded interpreter", .{args.path}); + + // Determine output path + const output_path = if (args.output) |output| + try ctx.arena.dupe(u8, output) + else blk: { + const basename = std.fs.path.basename(args.path); + const name_without_ext = if (std.mem.endsWith(u8, basename, ".roc")) + basename[0 .. basename.len - 4] + else + basename; + break :blk try ctx.arena.dupe(u8, name_without_ext); + }; + + // Set up cache directory for build artifacts + const cache_config = CacheConfig{ + .enabled = true, + .verbose = false, + }; + var cache_manager = CacheManager.init(ctx.gpa, cache_config, Filesystem.default()); + const cache_dir = try cache_manager.config.getCacheEntriesDir(ctx.arena); + const build_cache_dir = try std.fs.path.join(ctx.arena, &.{ cache_dir, "roc_build" }); + + std.fs.cwd().makePath(build_cache_dir) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; + + // Get platform directory and host library (do this first to get platform source) + const app_dir = std.fs.path.dirname(args.path) orelse "."; + // Extract platform spec - errors are recorded in context and propagate up + const platform_spec = try extractPlatformSpecFromApp(ctx, args.path); + std.log.debug("Platform spec: {s}", .{platform_spec}); + + // Resolve platform path - errors are recorded in context and propagate up + const platform_paths: ?PlatformPaths = if (std.mem.startsWith(u8, platform_spec, "./") or std.mem.startsWith(u8, platform_spec, "../")) + try resolvePlatformSpecToPaths(ctx, platform_spec, app_dir) + else if (std.mem.startsWith(u8, platform_spec, "http://") or std.mem.startsWith(u8, platform_spec, "https://")) + try resolvePlatformSpecToPaths(ctx, platform_spec, app_dir) + else + null; + + // Validate platform header has targets section and get link configuration + // The targets section is REQUIRED - it defines exactly what to link + const platform_source = if (platform_paths) |pp| pp.platform_source_path else null; + const validation = if (platform_source) |ps| + platform_validation.validatePlatformHeader(ctx.arena, ps) catch |err| { + switch (err) { + error.MissingTargetsSection => { + const result = platform_validation.ValidationResult{ + .missing_targets_section = .{ .platform_path = ps }, + }; + _ = platform_validation.renderValidationError(ctx.gpa, result, ctx.io.stderr()); + return error.MissingTargetsSection; + }, + else => { + renderProblem(ctx.gpa, ctx.io.stderr(), .{ + .platform_validation_failed = .{ + .message = "Failed to validate platform header", + }, + }); + return err; + }, + } + } + else { + renderProblem(ctx.gpa, ctx.io.stderr(), .{ + .no_platform_found = .{ .app_path = args.path }, + }); + return error.NoPlatformSource; + }; + + const targets_config = validation.config; + const platform_dir = validation.platform_dir; + + // Select target and link type + // If --target is provided, use that; otherwise find the first compatible target + const target: target_mod.RocTarget, const link_type: target_mod.LinkType = if (args.target) |target_str| blk: { + const parsed_target = target_mod.RocTarget.fromString(target_str) orelse { + const result = platform_validation.targets_validator.ValidationResult{ + .invalid_target = .{ .target_str = target_str }, + }; + _ = platform_validation.renderValidationError(ctx.gpa, result, ctx.io.stderr()); + return error.InvalidTarget; + }; + + // Find which link type supports this target (prefer exe > static_lib > shared_lib) + const lt: target_mod.LinkType = if (targets_config.supportsTarget(parsed_target, .exe)) + .exe + else if (targets_config.supportsTarget(parsed_target, .static_lib)) + .static_lib + else if (targets_config.supportsTarget(parsed_target, .shared_lib)) + .shared_lib + else { + const result = platform_validation.createUnsupportedTargetResult( + platform_source.?, + parsed_target, + .exe, // Show exe as the expected type for error message + targets_config, + ); + _ = platform_validation.renderValidationError(ctx.gpa, result, ctx.io.stderr()); + return error.UnsupportedTarget; + }; + + break :blk .{ parsed_target, lt }; + } else blk: { + // No --target provided: find the first compatible target across all link types + const compatible = targets_config.getFirstCompatibleTarget() orelse { + renderProblem(ctx.gpa, ctx.io.stderr(), .{ + .platform_validation_failed = .{ + .message = "No compatible target found. The platform does not support any target compatible with this system.", + }, + }); + return error.UnsupportedTarget; + }; + break :blk .{ compatible.target, compatible.link_type }; + }; + + std.log.debug("Target: {s}, Link type: {s}", .{ @tagName(target), @tagName(link_type) }); + + // Add appropriate file extension based on target and link type + const final_output_path = if (args.output != null) + output_path // User specified output, use as-is + else blk: { + // Auto-determine extension based on target + const ext = if (target == .wasm32) + ".wasm" + else if (target.isWindows()) + if (link_type == .exe) ".exe" else if (link_type == .shared_lib) ".dll" else ".lib" + else if (target.isMacOS()) + if (link_type == .shared_lib) ".dylib" else if (link_type == .static_lib) ".a" else "" + else if (link_type == .shared_lib) ".so" else if (link_type == .static_lib) ".a" else ""; + + if (ext.len > 0) { + break :blk try std.fmt.allocPrint(ctx.arena, "{s}{s}", .{ output_path, ext }); + } else { + break :blk output_path; + } + }; + + // Check for unsupported cross-compilation scenarios + const host_os = builtin.target.os.tag; + const host_ptr_width = @bitSizeOf(usize); + + // Always use portable serialization for roc build (embedded mode) + // The IPC format relies on shared memory alignment guarantees that don't apply + // when data is embedded in a binary at arbitrary addresses + const target_ptr_width = target.ptrBitWidth(); + + // Compile and serialize the module data using portable format + // This handles unaligned embedded data and cross-architecture builds correctly + std.log.debug("Compiling Roc file: {s}", .{args.path}); + const SerializedData = struct { + bytes: []const u8, + cleanup: ?ShmCleanup, + + const ShmCleanup = struct { + fd: if (is_windows) *anyopaque else c_int, + ptr: *anyopaque, + size: usize, + }; + }; + + std.log.debug("Using portable serialization ({d}-bit host -> {d}-bit target)", .{ host_ptr_width, target_ptr_width }); + + // Compile - errors are already reported by the compilation functions + const compile_result = try compileAndSerializeModulesForEmbedding(ctx, args.path, args.allow_errors); + std.log.debug("Portable serialization complete, {} bytes", .{compile_result.bytes.len}); + + const serialized_data: SerializedData = .{ + .bytes = compile_result.bytes, + .cleanup = null, // Arena-allocated, no cleanup needed + }; + + // Clean up shared memory when done (only if we used it) + defer if (serialized_data.cleanup) |cleanup| { + if (comptime is_windows) { + _ = ipc.platform.windows.UnmapViewOfFile(cleanup.ptr); + _ = ipc.platform.windows.CloseHandle(cleanup.fd); + } else { + _ = posix.munmap(cleanup.ptr, cleanup.size); + _ = c.close(cleanup.fd); + } + }; + + const serialized_module = serialized_data.bytes; + + // glibc targets require a full libc for linking, which is only available on Linux hosts + if (target.isDynamic() and host_os != .linux) { + const result = platform_validation.targets_validator.ValidationResult{ + .unsupported_glibc_cross = .{ + .target = target, + .host_os = @tagName(host_os), + }, + }; + _ = platform_validation.renderValidationError(ctx.gpa, result, ctx.io.stderr()); + return error.UnsupportedCrossCompilation; + } + + // Get the link spec for this target - tells us exactly what files to link + const link_spec = targets_config.getLinkSpec(target, link_type) orelse { + return ctx.fail(.{ .linker_failed = .{ + .err = error.UnsupportedTarget, + .target = @tagName(target), + } }); + }; + + // Build link file lists from the link spec + // Files before 'app' go in pre, files after 'app' go in post + const target_name = @tagName(target); + const files_dir = targets_config.files_dir orelse "targets"; + var platform_files_pre = try std.array_list.Managed([]const u8).initCapacity(ctx.arena, 8); + var platform_files_post = try std.array_list.Managed([]const u8).initCapacity(ctx.arena, 8); + var hit_app = false; + + for (link_spec.items) |item| { + switch (item) { + .file_path => |path| { + // Build full path: platform_dir/files_dir/target_name/path + const full_path = try std.fs.path.join(ctx.arena, &.{ platform_dir, files_dir, target_name, path }); + + // Validate the file exists + std.fs.cwd().access(full_path, .{}) catch { + const result = platform_validation.targets_validator.ValidationResult{ + .missing_target_file = .{ + .target = target, + .link_type = link_type, + .file_path = path, + .expected_full_path = full_path, + }, + }; + _ = platform_validation.renderValidationError(ctx.gpa, result, ctx.io.stderr()); + return error.MissingTargetFile; + }; + + if (!hit_app) { + try platform_files_pre.append(full_path); + } else { + try platform_files_post.append(full_path); + } + }, + .app => { + hit_app = true; + }, + .win_gui => { + // Windows subsystem flag - will be handled by linker + }, + } + } + + std.log.debug("Link spec: {} files before app, {} files after app", .{ platform_files_pre.items.len, platform_files_post.items.len }); + + // Extract entrypoints from the platform source file + std.log.debug("Extracting entrypoints from platform...", .{}); + var entrypoints = std.array_list.Managed([]const u8).initCapacity(ctx.arena, 16) catch { + return error.OutOfMemory; + }; + + extractEntrypointsFromPlatform(ctx, platform_source.?, &entrypoints) catch |err| { + return ctx.fail(.{ .entrypoint_extraction_failed = .{ + .path = platform_source.?, + .reason = @errorName(err), + } }); + }; + std.log.debug("Found {} entrypoints", .{entrypoints.items.len}); + + // Link everything together + // object_files = the Roc application files + // platform_files_pre/post = files declared in link spec before/after 'app' + var object_files = try std.array_list.Managed([]const u8).initCapacity(ctx.arena, 4); + + // Extract shim library (interpreter shim) - now works for both native and wasm32 targets + // Include target name in filename to support different targets in the same cache + const shim_filename = try std.fmt.allocPrint(ctx.arena, "libroc_shim_{s}.a", .{target_name}); + const shim_path = try std.fs.path.join(ctx.arena, &.{ build_cache_dir, shim_filename }); + + std.fs.cwd().access(shim_path, .{}) catch { + // Shim not found, extract it + // For roc build, use the target-specific shim for cross-compilation support + std.log.debug("Extracting shim library for target {s} to {s}...", .{ target_name, shim_path }); + extractReadRocFilePathShimLibrary(ctx, shim_path, target) catch |err| { + return ctx.fail(.{ .shim_generation_failed = .{ .err = err } }); + }; + }; + + // Generate platform host shim with embedded module data + // The shim provides roc__ functions and embeds serialized bytecode + const enable_debug = args.debug or (builtin.mode == .Debug); + std.log.debug("Generating platform host shim with {} bytes of embedded data (debug={})...", .{ serialized_module.len, enable_debug }); + const platform_shim_path = try generatePlatformHostShim(ctx, build_cache_dir, entrypoints.items, target, serialized_module, enable_debug); + std.log.debug("Platform shim generated: {?s}", .{platform_shim_path}); + + try object_files.append(shim_path); + if (platform_shim_path) |psp| { + try object_files.append(psp); + } + + // Extra linker args for system libraries (not platform-provided) + var extra_args = try std.array_list.Managed([]const u8).initCapacity(ctx.arena, 8); + if (target.isMacOS()) { + // macOS requires linking with system libraries + try extra_args.append("-lSystem"); + } + + const linker_mod = @import("linker.zig"); + const target_abi = if (target.isStatic()) linker_mod.TargetAbi.musl else linker_mod.TargetAbi.gnu; + const link_config = linker_mod.LinkConfig{ + .target_format = linker_mod.TargetFormat.detectFromOs(target.toOsTag()), + .object_files = object_files.items, + .platform_files_pre = platform_files_pre.items, + .platform_files_post = platform_files_post.items, + .extra_args = extra_args.items, + .output_path = final_output_path, + .target_abi = target_abi, + .target_os = target.toOsTag(), + .target_arch = target.toCpuArch(), + .wasm_initial_memory = args.wasm_memory orelse linker_mod.DEFAULT_WASM_INITIAL_MEMORY, + .wasm_stack_size = args.wasm_stack_size orelse linker_mod.DEFAULT_WASM_STACK_SIZE, + }; + + // Dump linker inputs to temp directory if requested + if (args.z_dump_linker) { + try dumpLinkerInputs(ctx, link_config); + } + + try linker_mod.link(ctx, link_config); + + // Print success message to stdout + // Add "./" prefix for relative paths to make it clear it's in the current directory + const display_path = if (std.fs.path.isAbsolute(final_output_path) or + std.mem.startsWith(u8, final_output_path, "./") or + std.mem.startsWith(u8, final_output_path, "../")) + final_output_path + else + try std.fmt.allocPrint(ctx.arena, "./{s}", .{final_output_path}); + + try ctx.io.stdout().print("Successfully built {s}\n", .{display_path}); +} + +/// Dump linker inputs to a temp directory for debugging linking issues. +/// Creates a directory with all input files copied and a README with the linker command. +fn dumpLinkerInputs(ctx: *CliContext, link_config: linker.LinkConfig) !void { + const stderr = ctx.io.stderr(); + + // Create temp directory with unique name based on timestamp + const timestamp = std.time.timestamp(); + const dir_name = try std.fmt.allocPrint(ctx.arena, "roc-linker-debug-{d}", .{timestamp}); + const dump_dir = try std.fs.path.join(ctx.arena, &.{ "/tmp", dir_name }); + + std.fs.cwd().makePath(dump_dir) catch |err| { + try stderr.print("Failed to create debug dump directory '{s}': {}\n", .{ dump_dir, err }); + return err; + }; + + // Track copied files for the README + var copied_files = try std.array_list.Managed(CopiedFile).initCapacity(ctx.arena, 16); + + // Copy platform_files_pre + for (link_config.platform_files_pre, 0..) |src, i| { + const basename = std.fs.path.basename(src); + const dest_name = try std.fmt.allocPrint(ctx.arena, "pre_{d}_{s}", .{ i, basename }); + const dest_path = try std.fs.path.join(ctx.arena, &.{ dump_dir, dest_name }); + std.fs.cwd().copyFile(src, std.fs.cwd(), dest_path, .{}) catch |err| { + try stderr.print("Warning: Failed to copy '{s}': {}\n", .{ src, err }); + continue; + }; + try copied_files.append(.{ .name = dest_name, .original = src, .category = "platform (pre-link)" }); + } + + // Copy object_files + for (link_config.object_files, 0..) |src, i| { + const basename = std.fs.path.basename(src); + const dest_name = try std.fmt.allocPrint(ctx.arena, "obj_{d}_{s}", .{ i, basename }); + const dest_path = try std.fs.path.join(ctx.arena, &.{ dump_dir, dest_name }); + std.fs.cwd().copyFile(src, std.fs.cwd(), dest_path, .{}) catch |err| { + try stderr.print("Warning: Failed to copy '{s}': {}\n", .{ src, err }); + continue; + }; + try copied_files.append(.{ .name = dest_name, .original = src, .category = "object file" }); + } + + // Copy platform_files_post + for (link_config.platform_files_post, 0..) |src, i| { + const basename = std.fs.path.basename(src); + const dest_name = try std.fmt.allocPrint(ctx.arena, "post_{d}_{s}", .{ i, basename }); + const dest_path = try std.fs.path.join(ctx.arena, &.{ dump_dir, dest_name }); + std.fs.cwd().copyFile(src, std.fs.cwd(), dest_path, .{}) catch |err| { + try stderr.print("Warning: Failed to copy '{s}': {}\n", .{ src, err }); + continue; + }; + try copied_files.append(.{ .name = dest_name, .original = src, .category = "platform (post-link)" }); + } + + // Generate the linker command string + const link_cmd = linker.formatLinkCommand(ctx, link_config) catch |err| { + try stderr.print("Warning: Failed to format linker command: {}\n", .{err}); + return; + }; + + // Build the file list for README + var file_list = std.array_list.Managed(u8).init(ctx.arena); + for (copied_files.items) |file| { + try file_list.writer().print(" {s}\n <- {s} ({s})\n", .{ file.name, file.original, file.category }); + } + + // Write README.txt with instructions + const readme_content = try std.fmt.allocPrint(ctx.arena, + \\Roc Linker Debug Dump + \\===================== + \\ + \\Target format: {s} + \\Target OS: {s} + \\Target arch: {s} + \\Output: {s} + \\ + \\Files ({d} copied): + \\{s} + \\ + \\To manually reproduce the link step: + \\ + \\ {s} + \\ + \\Note: The command above uses original file paths. The copied files + \\in this directory preserve original filenames for inspection. + \\ + , .{ + @tagName(link_config.target_format), + if (link_config.target_os) |os| @tagName(os) else "native", + if (link_config.target_arch) |arch| @tagName(arch) else "native", + link_config.output_path, + copied_files.items.len, + file_list.items, + link_cmd, + }); + + const readme_path = try std.fs.path.join(ctx.arena, &.{ dump_dir, "README.txt" }); + const readme_file = std.fs.cwd().createFile(readme_path, .{}) catch |err| { + try stderr.print("Warning: Failed to create README.txt: {}\n", .{err}); + return; + }; + defer readme_file.close(); + readme_file.writeAll(readme_content) catch |err| { + try stderr.print("Warning: Failed to write README.txt: {}\n", .{err}); + }; + + // Print summary to stderr + try stderr.print( + \\ + \\=== Linker debug dump === + \\Directory: {s} + \\Files: {d} copied + \\ + \\To reproduce: + \\ {s} + \\ + \\See {s}/README.txt for details + \\========================= + \\ + , .{ dump_dir, copied_files.items.len, link_cmd, dump_dir }); +} + +const CopiedFile = struct { + name: []const u8, + original: []const u8, + category: []const u8, +}; + /// Information about a test (expect statement) to be evaluated const ExpectTest = struct { expr_idx: can.CIR.Expr.Idx, region: base.Region, }; -/// Simple test environment for evaluating expects -const TestOpsEnv = struct { - allocator: Allocator, - interpreter: ?*Interpreter, - roc_ops: ?RocOps, - - fn init(allocator: Allocator) TestOpsEnv { - return TestOpsEnv{ - .allocator = allocator, - .interpreter = null, - .roc_ops = null, - }; - } - - fn setInterpreter(self: *TestOpsEnv, interp: *Interpreter) void { - self.interpreter = interp; - } - - fn get_ops(self: *TestOpsEnv) *RocOps { - if (self.roc_ops == null) { - self.roc_ops = RocOps{ - .env = @ptrCast(self), - .roc_alloc = testRocAlloc, - .roc_dealloc = testRocDealloc, - .roc_realloc = testRocRealloc, - .roc_dbg = testRocDbg, - .roc_expect_failed = testRocExpectFailed, - .roc_crashed = testRocCrashed, - .host_fns = undefined, // Not used in tests - }; - } - return &(self.roc_ops.?); - } - - fn deinit(self: *TestOpsEnv) void { - if (self.interpreter) |interp| { - if (interp.crash_message) |msg| { - // Only free if we allocated it (not a string literal) - if (!std.mem.eql(u8, msg, "Failed to store crash message")) { - self.allocator.free(msg); - } - } - } - } -}; - -fn testRocAlloc(alloc_args: *RocAlloc, env: *anyopaque) callconv(.C) void { - const test_env: *TestOpsEnv = @ptrCast(@alignCast(env)); - const align_enum = std.mem.Alignment.fromByteUnits(@as(usize, @intCast(alloc_args.alignment))); - const size_storage_bytes = @max(alloc_args.alignment, @alignOf(usize)); - const total_size = alloc_args.length + size_storage_bytes; - const result = test_env.allocator.rawAlloc(total_size, align_enum, @returnAddress()); - const base_ptr = result orelse { - std.debug.panic("Out of memory during testRocAlloc", .{}); - }; - const size_ptr: *usize = @ptrFromInt(@intFromPtr(base_ptr) + size_storage_bytes - @sizeOf(usize)); - size_ptr.* = total_size; - alloc_args.answer = @ptrFromInt(@intFromPtr(base_ptr) + size_storage_bytes); -} - -fn testRocDealloc(dealloc_args: *RocDealloc, env: *anyopaque) callconv(.C) void { - const test_env: *TestOpsEnv = @ptrCast(@alignCast(env)); - const size_storage_bytes = @max(dealloc_args.alignment, @alignOf(usize)); - const size_ptr: *const usize = @ptrFromInt(@intFromPtr(dealloc_args.ptr) - @sizeOf(usize)); - const total_size = size_ptr.*; - const base_ptr: [*]u8 = @ptrFromInt(@intFromPtr(dealloc_args.ptr) - size_storage_bytes); - const log2_align = std.math.log2_int(u32, @intCast(dealloc_args.alignment)); - const align_enum: std.mem.Alignment = @enumFromInt(log2_align); - const slice = @as([*]u8, @ptrCast(base_ptr))[0..total_size]; - test_env.allocator.rawFree(slice, align_enum, @returnAddress()); -} - -fn testRocRealloc(realloc_args: *RocRealloc, env: *anyopaque) callconv(.C) void { - const test_env: *TestOpsEnv = @ptrCast(@alignCast(env)); - const size_storage_bytes = @max(realloc_args.alignment, @alignOf(usize)); - const old_size_ptr: *const usize = @ptrFromInt(@intFromPtr(realloc_args.answer) - @sizeOf(usize)); - const old_total_size = old_size_ptr.*; - const old_base_ptr: [*]u8 = @ptrFromInt(@intFromPtr(realloc_args.answer) - size_storage_bytes); - const new_total_size = realloc_args.new_length + size_storage_bytes; - const old_slice = @as([*]u8, @ptrCast(old_base_ptr))[0..old_total_size]; - const new_slice = test_env.allocator.realloc(old_slice, new_total_size) catch { - std.debug.panic("Out of memory during testRocRealloc", .{}); - }; - const new_size_ptr: *usize = @ptrFromInt(@intFromPtr(new_slice.ptr) + size_storage_bytes - @sizeOf(usize)); - new_size_ptr.* = new_total_size; - realloc_args.answer = @ptrFromInt(@intFromPtr(new_slice.ptr) + size_storage_bytes); -} - -fn testRocDbg(dbg_args: *const RocDbg, env: *anyopaque) callconv(.C) void { - _ = dbg_args; - _ = env; - @panic("testRocDbg not implemented yet"); -} - -fn testRocExpectFailed(expect_args: *const RocExpectFailed, env: *anyopaque) callconv(.C) void { - _ = expect_args; - _ = env; - @panic("testRocExpectFailed not implemented yet"); -} - -fn testRocCrashed(crashed_args: *const RocCrashed, env: *anyopaque) callconv(.C) void { - const test_env: *TestOpsEnv = @ptrCast(@alignCast(env)); - const msg_slice = crashed_args.utf8_bytes[0..crashed_args.len]; - if (test_env.interpreter) |interp| { - interp.has_crashed = true; - const owned_msg = test_env.allocator.dupe(u8, msg_slice) catch |err| { - std.log.err("Failed to allocate crash message: {}", .{err}); - interp.crash_message = "Failed to store crash message"; - return; - }; - interp.crash_message = owned_msg; - } -} - -fn rocTest(gpa: Allocator, args: cli_args.TestArgs) !void { +fn rocTest(ctx: *CliContext, args: cli_args.TestArgs) !void { const trace = tracy.trace(@src()); defer trace.end(); // Start timing const start_time = std.time.nanoTimestamp(); - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); + const stdout = ctx.io.stdout(); + const stderr = ctx.io.stderr(); // Read the Roc file - const source = std.fs.cwd().readFileAlloc(gpa, args.path, std.math.maxInt(usize)) catch |err| { + var source = std.fs.cwd().readFileAlloc(ctx.gpa, args.path, std.math.maxInt(usize)) catch |err| { try stderr.print("Failed to read file '{s}': {}\n", .{ args.path, err }); - std.process.exit(1); + return err; }; - defer gpa.free(source); + source = base.source_utils.normalizeLineEndingsRealloc(ctx.gpa, source) catch |err| { + ctx.gpa.free(source); + return err; + }; + defer ctx.gpa.free(source); // Extract module name from the file path const basename = std.fs.path.basename(args.path); - const module_name = try gpa.dupe(u8, basename); - defer gpa.free(module_name); + const module_name = try ctx.arena.dupe(u8, basename); // Create ModuleEnv - var env = ModuleEnv.init(gpa, source) catch |err| { + var env = ModuleEnv.init(ctx.gpa, source) catch |err| { try stderr.print("Failed to initialize module environment: {}\n", .{err}); - std.process.exit(1); + return err; }; defer env.deinit(); env.common.source = source; env.module_name = module_name; - try env.common.calcLineStarts(gpa); + try env.common.calcLineStarts(ctx.gpa); + + // Load builtin modules required by the type checker and interpreter + const builtin_indices = builtin_loading.deserializeBuiltinIndices(ctx.gpa, compiled_builtins.builtin_indices_bin) catch |err| { + try stderr.print("Failed to deserialize builtin indices: {}\n", .{err}); + return err; + }; + const builtin_source = compiled_builtins.builtin_source; + var builtin_module = builtin_loading.loadCompiledModule(ctx.gpa, compiled_builtins.builtin_bin, "Builtin", builtin_source) catch |err| { + try stderr.print("Failed to load Builtin module: {}\n", .{err}); + return err; + }; + defer builtin_module.deinit(); + + // Populate module_envs with Bool, Try, Dict, Set from builtin module + var module_envs = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(ctx.gpa); + defer module_envs.deinit(); + + const module_builtin_ctx: Check.BuiltinContext = .{ + .module_name = try env.insertIdent(base.Ident.for_text(module_name)), + .bool_stmt = builtin_indices.bool_type, + .try_stmt = builtin_indices.try_type, + .str_stmt = builtin_indices.str_type, + .builtin_module = builtin_module.env, + .builtin_indices = builtin_indices, + }; // Parse the source code as a full module - var parse_ast = parse.parse(&env.common, gpa) catch |err| { + var parse_ast = parse.parse(&env.common, ctx.gpa) catch |err| { try stderr.print("Failed to parse file: {}\n", .{err}); - std.process.exit(1); + return err; }; - defer parse_ast.deinit(gpa); + defer parse_ast.deinit(ctx.gpa); // Empty scratch space (required before canonicalization) parse_ast.store.emptyScratch(); // Initialize CIR fields in ModuleEnv - try env.initCIRFields(gpa, module_name); + try env.initCIRFields(module_name); + + // Populate module_envs with Bool, Try, Dict, Set using shared function + try Can.populateModuleEnvs( + &module_envs, + &env, + builtin_module.env, + builtin_indices, + ); // Create canonicalizer - var canonicalizer = Can.init(&env, &parse_ast, null) catch |err| { + var canonicalizer = Can.init(&env, &parse_ast, &module_envs) catch |err| { try stderr.print("Failed to initialize canonicalizer: {}\n", .{err}); - std.process.exit(1); + return err; }; defer canonicalizer.deinit(); // Canonicalize the entire module canonicalizer.canonicalizeFile() catch |err| { try stderr.print("Failed to canonicalize file: {}\n", .{err}); - std.process.exit(1); + return err; }; + // Validate for checking mode + canonicalizer.validateForChecking() catch |err| { + try stderr.print("Failed to validate module: {}\n", .{err}); + return err; + }; + + // Build imported_envs array with builtin module + const imported_envs: []const *const ModuleEnv = &.{builtin_module.env}; + + // Resolve imports - map each import to its index in imported_envs + env.imports.resolveImports(&env, imported_envs); + // Type check the module - var checker = Check.init(gpa, &env.types, &env, &.{}, &env.store.regions) catch |err| { + var checker = Check.init(ctx.gpa, &env.types, &env, imported_envs, &module_envs, &env.store.regions, module_builtin_ctx) catch |err| { try stderr.print("Failed to initialize type checker: {}\n", .{err}); - std.process.exit(1); + return err; }; defer checker.deinit(); - checker.checkDefs() catch |err| { + checker.checkFile() catch |err| { try stderr.print("Type checking failed: {}\n", .{err}); - std.process.exit(1); + return err; }; - // Find all expect statements - const statements = env.store.sliceStatements(env.all_statements); - var expects = std.ArrayList(ExpectTest).init(gpa); - defer expects.deinit(); - - for (statements) |stmt_idx| { - const stmt = env.store.getStatement(stmt_idx); - if (stmt == .s_expect) { - const region = env.store.getStatementRegion(stmt_idx); - try expects.append(.{ - .expr_idx = stmt.s_expect.body, - .region = region, - }); - } - } - - if (expects.items.len == 0) { - try stdout.print("No tests found in {s}\n", .{args.path}); - return; - } - - // Create interpreter infrastructure for test evaluation - var stack_memory = eval.Stack.initCapacity(gpa, 1024) catch |err| { - try stderr.print("Failed to create stack memory: {}\n", .{err}); - std.process.exit(1); + // Evaluate all top-level declarations at compile time + const builtin_types_for_eval = BuiltinTypes.init(builtin_indices, builtin_module.env, builtin_module.env, builtin_module.env); + var comptime_evaluator = eval.ComptimeEvaluator.init(ctx.gpa, &env, imported_envs, &checker.problems, builtin_types_for_eval, builtin_module.env, &checker.import_mapping) catch |err| { + try stderr.print("Failed to create compile-time evaluator: {}\n", .{err}); + return err; }; - defer stack_memory.deinit(); + // Note: comptime_evaluator must be deinitialized AFTER building reports from checker.problems + // because the crash messages are owned by the evaluator but referenced by the problems - var layout_cache = LayoutStore.init(&env, &env.types) catch |err| { - try stderr.print("Failed to create layout cache: {}\n", .{err}); - std.process.exit(1); - }; - defer layout_cache.deinit(); - - var test_env = TestOpsEnv.init(gpa); - defer test_env.deinit(); - - var interpreter = Interpreter.init(gpa, &env, &stack_memory, &layout_cache, &env.types) catch |err| { - try stderr.print("Failed to create interpreter: {}\n", .{err}); - std.process.exit(1); - }; - defer interpreter.deinit(test_env.get_ops()); - test_env.setInterpreter(&interpreter); - - // Track test results for verbose output - const TestResult = struct { - line_number: u32, - passed: bool, - error_msg: ?[]const u8 = null, + _ = comptime_evaluator.evalAll() catch |err| { + try stderr.print("Failed to evaluate declarations: {}\n", .{err}); + return err; }; - var test_results = std.ArrayList(TestResult).init(gpa); - defer test_results.deinit(); + // Create test runner infrastructure for test evaluation (reuse builtin_types_for_eval from above) + var test_runner = TestRunner.init(ctx.gpa, &env, builtin_types_for_eval, imported_envs, builtin_module.env, &checker.import_mapping) catch |err| { + try stderr.print("Failed to create test runner: {}\n", .{err}); + return err; + }; + defer test_runner.deinit(); - // Evaluate each expect statement - var passed: u32 = 0; - var failed: u32 = 0; - - for (expects.items) |expect_test| { - const region_info = env.calcRegionInfo(expect_test.region); - const line_number = region_info.start_line_idx + 1; - - // Evaluate the expect expression - const result = interpreter.eval(expect_test.expr_idx, test_env.get_ops()) catch |err| { - const error_msg = try std.fmt.allocPrint(gpa, "Test evaluation failed: {}", .{err}); - try test_results.append(.{ .line_number = line_number, .passed = false, .error_msg = error_msg }); - failed += 1; - continue; - }; - - // Check if the result is a boolean true - if (result.layout.tag == .scalar and result.layout.data.scalar.tag == .bool) { - const is_true = result.asBool(); - if (is_true) { - try test_results.append(.{ .line_number = line_number, .passed = true }); - passed += 1; - } else { - try test_results.append(.{ .line_number = line_number, .passed = false }); - failed += 1; - } - } else { - const error_msg = try gpa.dupe(u8, "Test did not evaluate to a boolean"); - try test_results.append(.{ .line_number = line_number, .passed = false, .error_msg = error_msg }); - failed += 1; - } - } + const summary = test_runner.eval_all() catch |err| { + try stderr.print("Failed to evaluate tests: {}\n", .{err}); + return err; + }; + const passed = summary.passed; + const failed = summary.failed; // Calculate elapsed time const end_time = std.time.nanoTimestamp(); const elapsed_ns = @as(u64, @intCast(end_time - start_time)); const elapsed_ms = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000.0; - // Free allocated error messages - defer for (test_results.items) |test_result| { - if (test_result.error_msg) |msg| { - gpa.free(msg); + // Report any compile-time crashes + const has_comptime_crashes = checker.problems.len() > 0; + if (has_comptime_crashes) { + const problem = @import("check").problem; + var report_builder = problem.ReportBuilder.init( + ctx.gpa, + &env, + &env, + &checker.snapshots, + args.path, + &.{}, + &checker.import_mapping, + ); + defer report_builder.deinit(); + + for (0..checker.problems.len()) |i| { + const problem_idx: problem.Problem.Idx = @enumFromInt(i); + const prob = checker.problems.get(problem_idx); + var report = report_builder.build(prob) catch |err| { + try stderr.print("Failed to build problem report: {}\n", .{err}); + continue; + }; + defer report.deinit(); + + const palette = reporting.ColorUtils.getPaletteForConfig(reporting.ReportingConfig.initColorTerminal()); + const config = reporting.ReportingConfig.initColorTerminal(); + try reporting.renderReportToTerminal(&report, stderr, palette, config); } - }; + } + + // Clean up comptime evaluator AFTER building reports (crash messages must stay alive until reports are built) + comptime_evaluator.deinit(); // Report results - if (failed == 0) { - // Success case: only print if verbose, exit with 0 + if (failed == 0 and !has_comptime_crashes) { + // Success case: print summary + try stdout.print("All ({}) tests passed in {d:.1} ms.\n", .{ passed, elapsed_ms }); if (args.verbose) { - try stdout.print("Ran {} test(s): {} passed, 0 failed in {d:.1}ms\n", .{ passed, passed, elapsed_ms }); - for (test_results.items) |test_result| { - try stdout.print("PASS: line {}\n", .{test_result.line_number}); + // Generate and render a detailed report if verbose is true + for (test_runner.test_results.items) |test_result| { + const region_info = env.calcRegionInfo(test_result.region); + try stdout.print("\x1b[32mPASS\x1b[0m: {s}:{}\n", .{ args.path, region_info.start_line_idx + 1 }); } } - // Otherwise print nothing at all return; // Exit with 0 } else { // Failure case: always print summary with timing - try stderr.print("Ran {} test(s): {} passed, {} failed in {d:.1}ms\n", .{ passed + failed, passed, failed, elapsed_ms }); + const total_tests = passed + failed; + if (total_tests > 0) { + try stderr.print("Ran {} test(s): {} passed, {} failed in {d:.1}ms\n", .{ total_tests, passed, failed, elapsed_ms }); + } if (args.verbose) { - for (test_results.items) |test_result| { + for (test_runner.test_results.items) |test_result| { + const region_info = env.calcRegionInfo(test_result.region); if (test_result.passed) { - try stderr.print("PASS: line {}\n", .{test_result.line_number}); + try stdout.print("\x1b[32mPASS\x1b[0m: {s}:{}\n", .{ args.path, region_info.start_line_idx + 1 }); } else { - if (test_result.error_msg) |msg| { - try stderr.print("FAIL: line {} - {s}\n", .{ test_result.line_number, msg }); - } else { - try stderr.print("FAIL: line {}\n", .{test_result.line_number}); - } + // Generate and render a detailed report for this failure + var report = test_runner.createReport(test_result, args.path) catch |err| { + // Fallback to simple message if report generation fails + try stderr.print("\x1b[31mFAIL\x1b[0m: {s}:{}", .{ args.path, region_info.start_line_idx + 1 }); + if (test_result.error_msg) |msg| { + try stderr.print(" - {s}", .{msg}); + } + try stderr.print(" (report generation failed: {})\n", .{err}); + continue; + }; + defer report.deinit(); + + // Render the report to terminal + const palette = reporting.ColorUtils.getPaletteForConfig(reporting.ReportingConfig.initColorTerminal()); + const config = reporting.ReportingConfig.initColorTerminal(); + try reporting.renderReportToTerminal(&report, stderr, palette, config); + } + } + } else { + // Non-verbose mode: just show simple FAIL messages with line numbers + for (test_runner.test_results.items) |test_result| { + if (!test_result.passed) { + const region_info = env.calcRegionInfo(test_result.region); + try stderr.print("\x1b[31mFAIL\x1b[0m: {s}:{}\n", .{ args.path, region_info.start_line_idx + 1 }); } } } - std.process.exit(1); + return error.TestsFailed; } } -fn rocRepl(gpa: Allocator) !void { - _ = gpa; - fatal("repl not implemented", .{}); +fn rocRepl(ctx: *CliContext) !void { + ctx.io.stderr().print("repl not implemented\n", .{}) catch {}; + return error.NotImplemented; } /// Reads, parses, formats, and overwrites all Roc files at the given paths. /// Recurses into directories to search for Roc files. -fn rocFormat(gpa: Allocator, arena: Allocator, args: cli_args.FormatArgs) !void { +fn rocFormat(ctx: *CliContext, args: cli_args.FormatArgs) !void { const trace = tracy.trace(@src()); defer trace.end(); - const stdout = std.io.getStdOut(); + const stdout = ctx.io.stdout(); if (args.stdin) { - fmt.formatStdin(gpa) catch std.process.exit(1); + fmt.formatStdin(ctx.gpa) catch |err| return err; return; } var timer = try std.time.Timer.start(); var elapsed: u64 = undefined; var failure_count: usize = 0; - var exit_code: u8 = 0; + var had_errors: bool = false; if (args.check) { - var unformatted_files = std.ArrayList([]const u8).init(gpa); - defer unformatted_files.deinit(); + var unformatted_files = std.ArrayList([]const u8).empty; + defer unformatted_files.deinit(ctx.gpa); for (args.paths) |path| { - var result = try fmt.formatPath(gpa, arena, std.fs.cwd(), path, true); + var result = try fmt.formatPath(ctx.gpa, ctx.arena, std.fs.cwd(), path, true); defer result.deinit(); if (result.unformatted_files) |files| { - try unformatted_files.appendSlice(files.items); + try unformatted_files.appendSlice(ctx.gpa, files.items); } failure_count += result.failure; } elapsed = timer.read(); if (unformatted_files.items.len > 0) { - try stdout.writer().print("The following file(s) failed `roc format --check`:\n", .{}); + try stdout.print("The following file(s) failed `roc format --check`:", .{}); for (unformatted_files.items) |file_name| { - try stdout.writer().print(" {s}\n", .{file_name}); + try stdout.print(" {s}\n", .{file_name}); } - try stdout.writer().print("You can fix this with `roc format FILENAME.roc`.\n", .{}); - exit_code = 1; + try stdout.print("You can fix this with `roc format FILENAME.roc`.", .{}); + had_errors = true; } else { - try stdout.writer().print("All formatting valid\n", .{}); + try stdout.print("All formatting valid.\n", .{}); } if (failure_count > 0) { - try stdout.writer().print("Failed to check {} files.\n", .{failure_count}); - exit_code = 1; + try stdout.print("Failed to check {} files.", .{failure_count}); + had_errors = true; } } else { var success_count: usize = 0; for (args.paths) |path| { - const result = try fmt.formatPath(gpa, arena, std.fs.cwd(), path, false); + const result = try fmt.formatPath(ctx.gpa, ctx.arena, std.fs.cwd(), path, false); success_count += result.success; failure_count += result.failure; } elapsed = timer.read(); - try stdout.writer().print("Successfully formatted {} files\n", .{success_count}); + try stdout.print("Successfully formatted {} files\n", .{success_count}); if (failure_count > 0) { - try stdout.writer().print("Failed to format {} files.\n", .{failure_count}); - exit_code = 1; + try stdout.print("Failed to format {} files.\n", .{failure_count}); + had_errors = true; } } - try stdout.writer().print("Took ", .{}); - try formatElapsedTime(stdout.writer(), elapsed); - try stdout.writer().print(".\n", .{}); + try stdout.print("Took ", .{}); + try formatElapsedTime(stdout, elapsed); + try stdout.print(".\n", .{}); - std.process.exit(exit_code); + if (had_errors) { + return error.FormattingFailed; + } } /// Helper function to format elapsed time, showing decimal milliseconds @@ -1785,7 +4850,7 @@ fn formatElapsedTime(writer: anytype, elapsed_ns: u64) !void { try writer.print("{d:.1} ms", .{elapsed_ms_float}); } -fn handleProcessFileError(err: anytype, stderr: anytype, path: []const u8) noreturn { +fn handleProcessFileError(err: anytype, stderr: anytype, path: []const u8) !void { stderr.print("Failed to check {s}: ", .{path}) catch {}; switch (err) { // Custom BuildEnv errors - these need special messages @@ -1802,7 +4867,8 @@ fn handleProcessFileError(err: anytype, stderr: anytype, path: []const u8) noret // Catch-all for any other errors else => stderr.print("{s}\n", .{@errorName(err)}) catch {}, } - std.process.exit(1); + + return err; } /// Result from checking a file using BuildEnv @@ -1861,34 +4927,105 @@ const BuildAppError = std.mem.Allocator.Error || std.fs.File.OpenError || std.fs // Additional errors from std library that might be missing Unseekable, CurrentWorkingDirectoryUnlinked, + // URL package resolution errors + FileError, + InvalidUrl, + NoCacheDir, + DownloadFailed, + NoPackageSource, + // Interpreter errors (propagate from eval during build) + Crash, + DivisionByZero, + EarlyReturn, + IntegerOverflow, + InvalidImportIndex, + InvalidMethodReceiver, + InvalidNumExt, + InvalidTagExt, + ListIndexOutOfBounds, + MethodLookupFailed, + MethodNotFound, + NotImplemented, + NotNumeric, + NullStackPointer, + RecordIndexOutOfBounds, + StackOverflow, + StringOrderingNotSupported, + TupleIndexOutOfBounds, + TypeMismatch, + UnresolvedImport, + ZeroSizedType, + // Layout errors + TypeContainedMismatch, + InvalidRecordExtension, + InvalidNumberExtension, + BugUnboxedFlexVar, + BugUnboxedRigidVar, }; -/// Check a Roc file using the BuildEnv system -fn checkFileWithBuildEnv( - gpa: Allocator, +/// Result from checking a file that preserves the BuildEnv for further processing (e.g., docs generation) +const CheckResultWithBuildEnv = struct { + check_result: CheckResult, + build_env: BuildEnv, + + /// Free allocated memory including the BuildEnv + pub fn deinit(self: *CheckResultWithBuildEnv, gpa: Allocator) void { + self.check_result.deinit(gpa); + self.build_env.deinit(); + } +}; + +/// Check a Roc file using BuildEnv and preserve the BuildEnv for further processing +fn checkFileWithBuildEnvPreserved( + ctx: *CliContext, filepath: []const u8, collect_timing: bool, cache_config: CacheConfig, -) BuildAppError!CheckResult { +) BuildAppError!CheckResultWithBuildEnv { _ = collect_timing; // Timing is always collected by BuildEnv const trace = tracy.trace(@src()); defer trace.end(); // Initialize BuildEnv in single-threaded mode for checking - var build_env = BuildEnv.init(gpa, .single_threaded, 1); + var build_env = try BuildEnv.init(ctx.gpa, .single_threaded, 1); build_env.compiler_version = build_options.compiler_version; - defer build_env.deinit(); + // Note: We do NOT defer build_env.deinit() here because we're returning it // Set up cache manager if caching is enabled if (cache_config.enabled) { - const cache_manager = try gpa.create(CacheManager); - cache_manager.* = CacheManager.init(gpa, cache_config, Filesystem.default()); + const cache_manager = try ctx.gpa.create(CacheManager); + cache_manager.* = CacheManager.init(ctx.gpa, cache_config, Filesystem.default()); build_env.setCacheManager(cache_manager); - // Note: BuildEnv.deinit() will clean up the cache manager + // Note: BuildEnv.deinit() will clean up the cache manager when caller calls deinit } // Build the file (works for both app and module files) - try build_env.build(filepath); + build_env.build(filepath) catch |err| { + // Even on error, try to drain and print any reports that were collected + const drained = build_env.drainReports() catch &[_]BuildEnv.DrainedModuleReports{}; + defer build_env.gpa.free(drained); + + // Print any error reports to stderr before failing + return err; + }; + + // Force processing to ensure canonicalization happens + var sched_iter = build_env.schedulers.iterator(); + if (sched_iter.next()) |sched_entry| { + const package_env = sched_entry.value_ptr.*; + if (package_env.modules.items.len > 0) { + const module_name = package_env.modules.items[0].name; + + // Keep processing until the module is done + var max_iterations: u32 = 20; + while (max_iterations > 0) : (max_iterations -= 1) { + const phase = package_env.modules.items[0].phase; + if (phase == .Done) break; + + package_env.processModuleByName(module_name) catch break; + } + } + } // Drain all reports const drained = try build_env.drainReports(); @@ -1908,17 +5045,128 @@ fn checkFileWithBuildEnv( } // Convert BuildEnv drained reports to our format - var reports = try gpa.alloc(DrainedReport, drained.len); + var reports = try ctx.gpa.alloc(DrainedReport, drained.len); for (drained, 0..) |mod, i| { reports[i] = .{ - .file_path = try gpa.dupe(u8, mod.abs_path), - .reports = try gpa.dupe(reporting.Report, mod.reports), + .file_path = try ctx.gpa.dupe(u8, mod.abs_path), + .reports = mod.reports, // Transfer ownership }; } // Free the original drained reports // Note: abs_path is owned by BuildEnv, reports are moved to our array - gpa.free(drained); + ctx.gpa.free(drained); + + // Get timing information from BuildEnv + const timing = if (builtin.target.cpu.arch == .wasm32) + CheckTimingInfo{} + else + build_env.getTimingInfo(); + + const check_result = CheckResult{ + .reports = reports, + .timing = timing, + .was_cached = false, // BuildEnv doesn't currently expose cache info + .error_count = error_count, + .warning_count = warning_count, + }; + + return CheckResultWithBuildEnv{ + .check_result = check_result, + .build_env = build_env, + }; +} + +/// Check a Roc file using the BuildEnv system +fn checkFileWithBuildEnv( + ctx: *CliContext, + filepath: []const u8, + collect_timing: bool, + cache_config: CacheConfig, +) BuildAppError!CheckResult { + _ = collect_timing; // Timing is always collected by BuildEnv + const trace = tracy.trace(@src()); + defer trace.end(); + + // Initialize BuildEnv in single-threaded mode for checking + var build_env = try BuildEnv.init(ctx.gpa, .single_threaded, 1); + build_env.compiler_version = build_options.compiler_version; + defer build_env.deinit(); + + // Set up cache manager if caching is enabled + if (cache_config.enabled) { + const cache_manager = try ctx.gpa.create(CacheManager); + cache_manager.* = CacheManager.init(ctx.gpa, cache_config, Filesystem.default()); + build_env.setCacheManager(cache_manager); + // Note: BuildEnv.deinit() will clean up the cache manager + } + + // Build the file (works for both app and module files) + build_env.build(filepath) catch { + // Even on error, drain reports to show what went wrong + const drained = build_env.drainReports() catch &[_]BuildEnv.DrainedModuleReports{}; + defer build_env.gpa.free(drained); + + // Count errors and warnings + var error_count: u32 = 0; + var warning_count: u32 = 0; + + for (drained) |mod| { + for (mod.reports) |report| { + switch (report.severity) { + .info => {}, + .runtime_error, .fatal => error_count += 1, + .warning => warning_count += 1, + } + } + } + + // Convert BuildEnv drained reports to our format + // Note: Transfer ownership of reports since drainReports() already transferred them + var reports = try build_env.gpa.alloc(DrainedReport, drained.len); + for (drained, 0..) |mod, i| { + reports[i] = .{ + .file_path = try build_env.gpa.dupe(u8, mod.abs_path), + .reports = mod.reports, // Transfer ownership + }; + } + + return CheckResult{ + .reports = reports, + .error_count = error_count, + .warning_count = warning_count, + }; + }; + + // Drain all reports + const drained = try build_env.drainReports(); + + // Count errors and warnings + var error_count: u32 = 0; + var warning_count: u32 = 0; + + for (drained) |mod| { + for (mod.reports) |report| { + switch (report.severity) { + .info => {}, + .runtime_error, .fatal => error_count += 1, + .warning => warning_count += 1, + } + } + } + + // Convert BuildEnv drained reports to our format + var reports = try ctx.gpa.alloc(DrainedReport, drained.len); + for (drained, 0..) |mod, i| { + reports[i] = .{ + .file_path = try ctx.gpa.dupe(u8, mod.abs_path), + .reports = mod.reports, // Transfer ownership + }; + } + + // Free the original drained reports + // Note: abs_path is owned by BuildEnv, reports are moved to our array + ctx.gpa.free(drained); // Get timing information from BuildEnv const timing = if (builtin.target.cpu.arch == .wasm32) @@ -1935,13 +5183,12 @@ fn checkFileWithBuildEnv( }; } -fn rocCheck(gpa: Allocator, args: cli_args.CheckArgs) !void { +fn rocCheck(ctx: *CliContext, args: cli_args.CheckArgs) !void { const trace = tracy.trace(@src()); defer trace.end(); - const stdout = std.io.getStdOut().writer(); - const stderr = std.io.getStdErr().writer(); - const stderr_writer = stderr.any(); + const stdout = ctx.io.stdout(); + const stderr = ctx.io.stderr(); var timer = try std.time.Timer.start(); @@ -1953,15 +5200,15 @@ fn rocCheck(gpa: Allocator, args: cli_args.CheckArgs) !void { // Use BuildEnv to check the file var check_result = checkFileWithBuildEnv( - gpa, + ctx, args.path, args.time, cache_config, ) catch |err| { - handleProcessFileError(err, stderr, args.path); + try handleProcessFileError(err, stderr, args.path); + return; }; - - defer check_result.deinit(gpa); + defer check_result.deinit(ctx.gpa); const elapsed = timer.read(); @@ -1977,12 +5224,12 @@ fn rocCheck(gpa: Allocator, args: cli_args.CheckArgs) !void { total_warnings, }) catch {}; formatElapsedTime(stderr, elapsed) catch {}; - stderr.print(" for {s} (note module loaded from cache, use --no-cache to display Errors and Warnings.).\n", .{args.path}) catch {}; - std.process.exit(1); + stderr.print(" for {s} (note module loaded from cache, use --no-cache to display Errors and Warnings.).", .{args.path}) catch {}; + return error.CheckFailed; } else { stdout.print("No errors found in ", .{}) catch {}; formatElapsedTime(stdout, elapsed) catch {}; - stdout.print(" for {s} (loaded from cache)\n", .{args.path}) catch {}; + stdout.print(" for {s} (loaded from cache)", .{args.path}) catch {}; } } else { // For fresh compilation, process and display reports normally @@ -1993,10 +5240,265 @@ fn rocCheck(gpa: Allocator, args: cli_args.CheckArgs) !void { for (module.reports) |*report| { // Render the diagnostic report to stderr - reporting.renderReportToTerminal(report, stderr_writer, ColorPalette.ANSI, reporting.ReportingConfig.initColorTerminal()) catch |render_err| { - stderr.print("Error rendering diagnostic report: {}\n", .{render_err}) catch {}; + try reporting.renderReportToTerminal(report, stderr, ColorPalette.ANSI, reporting.ReportingConfig.initColorTerminal()); + + if (report.severity == .fatal or report.severity == .runtime_error) { + has_errors = true; + } + } + } + + // Flush stderr to ensure all error output is visible + ctx.io.flush(); + + if (check_result.error_count > 0 or check_result.warning_count > 0) { + stderr.writeAll("\n") catch {}; + stderr.print("Found {} error(s) and {} warning(s) in ", .{ + check_result.error_count, + check_result.warning_count, + }) catch {}; + formatElapsedTime(stderr, elapsed) catch {}; + stderr.print(" for {s}.\n", .{args.path}) catch {}; + + // Flush before exit + ctx.io.flush(); + return error.CheckFailed; + } else { + stdout.print("No errors found in ", .{}) catch {}; + formatElapsedTime(stdout, elapsed) catch {}; + stdout.print(" for {s}\n", .{args.path}) catch {}; + ctx.io.flush(); + } + } + + // Print timing breakdown if requested + if (args.time) { + printTimingBreakdown(stdout, if (builtin.target.cpu.arch == .wasm32) null else check_result.timing); + } +} + +fn printTimingBreakdown(writer: anytype, timing: ?CheckTimingInfo) void { + if (timing) |t| { + writer.print("\nTiming breakdown:", .{}) catch {}; + writer.print(" tokenize + parse: ", .{}) catch {}; + formatElapsedTime(writer, t.tokenize_parse_ns) catch {}; + writer.print(" ({} ns)", .{t.tokenize_parse_ns}) catch {}; + writer.print(" canonicalize: ", .{}) catch {}; + formatElapsedTime(writer, t.canonicalize_ns) catch {}; + writer.print(" ({} ns)", .{t.canonicalize_ns}) catch {}; + writer.print(" can diagnostics: ", .{}) catch {}; + formatElapsedTime(writer, t.canonicalize_diagnostics_ns) catch {}; + writer.print(" ({} ns)", .{t.canonicalize_diagnostics_ns}) catch {}; + writer.print(" type checking: ", .{}) catch {}; + formatElapsedTime(writer, t.type_checking_ns) catch {}; + writer.print(" ({} ns)", .{t.type_checking_ns}) catch {}; + writer.print(" type checking diagnostics: ", .{}) catch {}; + formatElapsedTime(writer, t.check_diagnostics_ns) catch {}; + writer.print(" ({} ns)", .{t.check_diagnostics_ns}) catch {}; + } +} + +/// Start an HTTP server to serve the generated documentation +fn serveDocumentation(ctx: *CliContext, docs_dir: []const u8) !void { + const stdout = ctx.io.stdout(); + + const address = try std.net.Address.parseIp("127.0.0.1", 8080); + var server = try address.listen(.{ + .reuse_address = true, + }); + defer server.deinit(); + + stdout.print("Visit http://localhost:8080 to view the docs at ./{s}/\n", .{docs_dir}) catch {}; + stdout.print("Press Ctrl+C to stop the server\n", .{}) catch {}; + + while (true) { + const connection = try server.accept(); + handleConnection(ctx, connection, docs_dir) catch |err| { + std.debug.print("Error handling connection: {}\n", .{err}); + }; + } +} + +/// Handle a single HTTP connection +fn handleConnection(ctx: *CliContext, connection: std.net.Server.Connection, docs_dir: []const u8) !void { + defer connection.stream.close(); + + var buffer: [4096]u8 = undefined; + var reader_buffer: [512]u8 = undefined; + var conn_reader = connection.stream.reader(&reader_buffer); + var slices = [_][]u8{buffer[0..]}; + const bytes_read = std.Io.Reader.readVec(conn_reader.interface(), &slices) catch |err| switch (err) { + error.EndOfStream => 0, + error.ReadFailed => return conn_reader.getError() orelse error.Unexpected, + }; + + if (bytes_read == 0) return; + + const request = buffer[0..bytes_read]; + + // Parse the request line (e.g., "GET /path HTTP/1.1") + var lines = std.mem.splitSequence(u8, request, "\r\n"); + const request_line = lines.next() orelse return; + + var parts = std.mem.splitSequence(u8, request_line, " "); + const method = parts.next() orelse return; + const path = parts.next() orelse return; + + if (!std.mem.eql(u8, method, "GET")) { + try sendResponse(connection.stream, "405 Method Not Allowed", "text/plain", "Method Not Allowed"); + return; + } + + // Determine the file path to serve + const file_path = try resolveFilePath(ctx, docs_dir, path); + + // Try to open and serve the file + const file = std.fs.cwd().openFile(file_path, .{}) catch |err| { + if (err == error.FileNotFound) { + try sendResponse(connection.stream, "404 Not Found", "text/plain", "File Not Found"); + } else { + try sendResponse(connection.stream, "500 Internal Server Error", "text/plain", "Internal Server Error"); + } + return; + }; + defer file.close(); + + // Read file contents + const file_content = try file.readToEndAlloc(ctx.gpa, 10 * 1024 * 1024); // 10MB max + defer ctx.gpa.free(file_content); + + // Determine content type + const content_type = getContentType(file_path); + + // Send response + try sendResponse(connection.stream, "200 OK", content_type, file_content); +} + +/// Resolve the file path based on the URL path. +/// Returns arena-allocated path (no need to free). +fn resolveFilePath(ctx: *CliContext, docs_dir: []const u8, url_path: []const u8) ![]const u8 { + // Remove leading slash + const clean_path = if (url_path.len > 0 and url_path[0] == '/') + url_path[1..] + else + url_path; + + // If path is empty or ends with /, serve index.html + if (clean_path.len == 0 or clean_path[clean_path.len - 1] == '/') { + return try std.fmt.allocPrint(ctx.arena, "{s}/{s}index.html", .{ docs_dir, clean_path }); + } + + // Check if the path has a file extension (contains a dot in the last component) + const last_slash = std.mem.lastIndexOfScalar(u8, clean_path, '/') orelse 0; + const last_component = clean_path[last_slash..]; + const has_extension = std.mem.indexOfScalar(u8, last_component, '.') != null; + + if (has_extension) { + // Path has extension, serve the file directly + return try std.fmt.allocPrint(ctx.arena, "{s}/{s}", .{ docs_dir, clean_path }); + } else { + // No extension, serve index.html from that directory + return try std.fmt.allocPrint(ctx.arena, "{s}/{s}/index.html", .{ docs_dir, clean_path }); + } +} + +/// Get content type based on file extension +fn getContentType(file_path: []const u8) []const u8 { + if (std.mem.endsWith(u8, file_path, ".html")) { + return "text/html; charset=utf-8"; + } else if (std.mem.endsWith(u8, file_path, ".css")) { + return "text/css"; + } else if (std.mem.endsWith(u8, file_path, ".js")) { + return "application/javascript"; + } else if (std.mem.endsWith(u8, file_path, ".json")) { + return "application/json"; + } else if (std.mem.endsWith(u8, file_path, ".png")) { + return "image/png"; + } else if (std.mem.endsWith(u8, file_path, ".jpg") or std.mem.endsWith(u8, file_path, ".jpeg")) { + return "image/jpeg"; + } else if (std.mem.endsWith(u8, file_path, ".svg")) { + return "image/svg+xml"; + } else { + return "text/plain"; + } +} + +/// Send an HTTP response +fn sendResponse(stream: std.net.Stream, status: []const u8, content_type: []const u8, body: []const u8) !void { + var response_buffer: [8192]u8 = undefined; + const response = try std.fmt.bufPrint( + &response_buffer, + "HTTP/1.1 {s}\r\n" ++ + "Content-Type: {s}\r\n" ++ + "Content-Length: {d}\r\n" ++ + "Connection: close\r\n" ++ + "\r\n", + .{ status, content_type, body.len }, + ); + + try stream.writeAll(response); + try stream.writeAll(body); +} + +fn rocDocs(ctx: *CliContext, args: cli_args.DocsArgs) !void { + const trace = tracy.trace(@src()); + defer trace.end(); + + const stdout = ctx.io.stdout(); + const stderr = ctx.io.stderr(); + + var timer = try std.time.Timer.start(); + + // Set up cache configuration based on command line args + const cache_config = CacheConfig{ + .enabled = !args.no_cache, + .verbose = args.verbose, + }; + + // Use BuildEnv to check the file, preserving the BuildEnv for docs generation + var result_with_env = checkFileWithBuildEnvPreserved( + ctx, + args.path, + args.time, + cache_config, + ) catch |err| { + return handleProcessFileError(err, stderr, args.path); + }; + + // Clean up when we're done - this includes the BuildEnv and all module envs + defer result_with_env.deinit(ctx.gpa); + + const check_result = &result_with_env.check_result; + const elapsed = timer.read(); + + // Handle cached results vs fresh compilation results differently + if (check_result.was_cached) { + // For cached results, use the stored diagnostic counts + const total_errors = check_result.error_count; + const total_warnings = check_result.warning_count; + + if (total_errors > 0 or total_warnings > 0) { + stderr.print("Found {} error(s) and {} warning(s) in ", .{ + total_errors, + total_warnings, + }) catch {}; + formatElapsedTime(stderr, elapsed) catch {}; + stderr.print(" for {s} (note module loaded from cache, use --no-cache to display Errors and Warnings.).", .{args.path}) catch {}; + return error.DocsFailed; + } + } else { + // For fresh compilation, process and display reports normally + var has_errors = false; + + // Render reports grouped by module + for (check_result.reports) |module| { + for (module.reports) |*report| { + + // Render the diagnostic report to stderr + reporting.renderReportToTerminal(report, stderr, ColorPalette.ANSI, reporting.ReportingConfig.initColorTerminal()) catch |render_err| { + stderr.print("Error rendering diagnostic report: {}", .{render_err}) catch {}; // Fallback to just printing the title - stderr.print(" {s}\n", .{report.title}) catch {}; + stderr.print(" {s}", .{report.title}) catch {}; }; if (report.severity == .fatal or report.severity == .runtime_error) { @@ -2012,13 +5514,11 @@ fn rocCheck(gpa: Allocator, args: cli_args.CheckArgs) !void { check_result.warning_count, }) catch {}; formatElapsedTime(stderr, elapsed) catch {}; - stderr.print(" for {s}.\n", .{args.path}) catch {}; + stderr.print(" for {s}.", .{args.path}) catch {}; - std.process.exit(1); - } else { - stdout.print("No errors found in ", .{}) catch {}; - formatElapsedTime(stdout, elapsed) catch {}; - stdout.print(" for {s}\n", .{args.path}) catch {}; + if (check_result.error_count > 0) { + return error.DocsFailed; + } } } @@ -2026,45 +5526,572 @@ fn rocCheck(gpa: Allocator, args: cli_args.CheckArgs) !void { if (args.time) { printTimingBreakdown(stdout, if (builtin.target.cpu.arch == .wasm32) null else check_result.timing); } -} -fn printTimingBreakdown(writer: anytype, timing: ?CheckTimingInfo) void { - if (timing) |t| { - writer.print("\nTiming breakdown:\n", .{}) catch {}; - writer.print(" tokenize + parse: ", .{}) catch {}; - formatElapsedTime(writer, t.tokenize_parse_ns) catch {}; - writer.print(" ({} ns)\n", .{t.tokenize_parse_ns}) catch {}; - writer.print(" canonicalize: ", .{}) catch {}; - formatElapsedTime(writer, t.canonicalize_ns) catch {}; - writer.print(" ({} ns)\n", .{t.canonicalize_ns}) catch {}; - writer.print(" can diagnostics: ", .{}) catch {}; - formatElapsedTime(writer, t.canonicalize_diagnostics_ns) catch {}; - writer.print(" ({} ns)\n", .{t.canonicalize_diagnostics_ns}) catch {}; - writer.print(" type checking: ", .{}) catch {}; - formatElapsedTime(writer, t.type_checking_ns) catch {}; - writer.print(" ({} ns)\n", .{t.type_checking_ns}) catch {}; - writer.print(" type checking diagnostics: ", .{}) catch {}; - formatElapsedTime(writer, t.check_diagnostics_ns) catch {}; - writer.print(" ({} ns)\n", .{t.check_diagnostics_ns}) catch {}; + // Generate documentation for all packages and modules + try generateDocs(ctx, &result_with_env.build_env, args.path, args.output); + + stdout.print("\nDocumentation generation complete for {s}\n", .{args.path}) catch {}; + + // Start HTTP server if --serve flag is enabled + if (args.serve) { + try serveDocumentation(ctx, args.output); } } -fn rocDocs(gpa: Allocator, args: cli_args.DocsArgs) !void { - _ = gpa; - _ = args; - fatal("docs not implemented", .{}); -} +/// Associated item (type or value) within a module or type +pub const AssociatedItem = struct { + name: []const u8, + children: []AssociatedItem, // Nested associated items (for types with associated items) -/// Log a fatal error and exit the process with a non-zero code. -pub fn fatal(comptime format: []const u8, args: anytype) noreturn { - std.io.getStdErr().writer().print(format, args) catch unreachable; - if (tracy.enable) { - tracy.waitForShutdown() catch unreachable; + fn deinit(self: AssociatedItem, gpa: Allocator) void { + gpa.free(self.name); + for (self.children) |child| { + child.deinit(gpa); + } + gpa.free(self.children); } - std.process.exit(1); +}; + +/// Information about an imported module +pub const ModuleInfo = struct { + name: []const u8, // e.g., "Foo" or "foo.Bar" + link_path: []const u8, // e.g., "Foo" or "foo/Bar" + associated_items: []AssociatedItem, // Types and values defined in this module + + fn deinit(self: ModuleInfo, gpa: Allocator) void { + gpa.free(self.name); + gpa.free(self.link_path); + for (self.associated_items) |item| { + item.deinit(gpa); + } + gpa.free(self.associated_items); + } +}; + +/// Recursively write associated items as nested
    elements +fn writeAssociatedItems(writer: anytype, items: []const AssociatedItem, indent_level: usize) !void { + // Write opening
      + try writer.splatByteAll(' ', indent_level * 2); + try writer.writeAll("
        \n"); + + for (items) |item| { + // Write
      • with item name + try writer.splatByteAll(' ', (indent_level + 1) * 2); + try writer.print("
      • {s}\n", .{item.name}); + + // Recursively write children if any + if (item.children.len > 0) { + try writeAssociatedItems(writer, item.children, indent_level + 2); + } + + // Close
      • + try writer.splatByteAll(' ', (indent_level + 1) * 2); + try writer.writeAll("
      • \n"); + } + + // Write closing
      + try writer.splatByteAll(' ', indent_level * 2); + try writer.writeAll("
    \n"); } -// Include tests from other files -test { - _ = @import("test_shared_memory_system.zig"); +/// Generate HTML index file for a package or app +pub fn generatePackageIndex( + ctx: *CliContext, + output_path: []const u8, + module_path: []const u8, + package_shorthands: []const []const u8, + imported_modules: []const ModuleInfo, +) !void { + // Create output directory if it doesn't exist + std.fs.cwd().makePath(output_path) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; + + // Create index.html file + const index_path = try std.fs.path.join(ctx.arena, &[_][]const u8{ output_path, "index.html" }); + + const file = try std.fs.cwd().createFile(index_path, .{}); + defer file.close(); + + var file_buffer: [4096]u8 = undefined; + var file_writer = file.writer(&file_buffer); + const writer = &file_writer.interface; + + // Write HTML header + try writer.writeAll("\n\n\n"); + try writer.writeAll(" \n"); + try writer.writeAll(" Documentation\n"); + try writer.writeAll("\n\n"); + + // Write module path as h1 + try writer.print("

    {s}

    \n", .{module_path}); + + // Write sidebar with imported modules if any + if (imported_modules.len > 0) { + try writer.writeAll(" \n"); + } + + // Write links to package dependencies if any exist + if (package_shorthands.len > 0) { + try writer.writeAll("
      \n"); + for (package_shorthands) |shorthand| { + try writer.print("
    • {s}
    • \n", .{ shorthand, shorthand }); + } + try writer.writeAll("
    \n"); + } + + try writer.writeAll("\n\n"); + try writer.flush(); +} + +/// Generate HTML index file for a module +pub fn generateModuleIndex( + ctx: *CliContext, + output_path: []const u8, + module_name: []const u8, +) !void { + // Create output directory if it doesn't exist + std.fs.cwd().makePath(output_path) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; + + // Create index.html file + const index_path = try std.fs.path.join(ctx.arena, &[_][]const u8{ output_path, "index.html" }); + + const file = try std.fs.cwd().createFile(index_path, .{}); + defer file.close(); + + var file_buffer: [4096]u8 = undefined; + var file_writer = file.writer(&file_buffer); + const writer = &file_writer.interface; + + // Write HTML header + try writer.writeAll("\n\n\n"); + try writer.writeAll(" \n"); + try writer.print(" {s}\n", .{module_name}); + try writer.writeAll("\n\n"); + + // Write module name as h1 + try writer.print("

    {s}

    \n", .{module_name}); + + try writer.writeAll("\n\n"); + try writer.flush(); +} + +/// Extract associated items from a record expression (recursively) +fn extractRecordAssociatedItems( + ctx: *CliContext, + module_env: *const ModuleEnv, + record_fields: can.CIR.RecordField.Span, +) ![]AssociatedItem { + var items = std.array_list.Managed(AssociatedItem).init(ctx.gpa); + errdefer { + for (items.items) |item| { + item.deinit(ctx.gpa); + } + items.deinit(); + } + + const fields_slice = module_env.store.sliceRecordFields(record_fields); + for (fields_slice) |field_idx| { + const field = module_env.store.getRecordField(field_idx); + const field_name = try ctx.gpa.dupe(u8, module_env.getIdentText(field.name)); + errdefer ctx.gpa.free(field_name); + + // Check if the field value is a nominal type (has nested associated items) + const field_expr = module_env.store.getExpr(field.value); + const children = switch (field_expr) { + .e_nominal => |nom| blk: { + // Get the nominal type's backing expression + const backing_expr = module_env.store.getExpr(nom.backing_expr); + break :blk switch (backing_expr) { + .e_record => |rec| try extractRecordAssociatedItems(ctx, module_env, rec.fields), + else => try ctx.gpa.alloc(AssociatedItem, 0), + }; + }, + else => try ctx.gpa.alloc(AssociatedItem, 0), + }; + + try items.append(.{ + .name = field_name, + .children = children, + }); + } + + return try items.toOwnedSlice(); +} + +/// Extract associated items from a module's exports +fn extractAssociatedItems( + ctx: *CliContext, + module_env: *const ModuleEnv, +) ![]AssociatedItem { + var items = std.array_list.Managed(AssociatedItem).init(ctx.gpa); + errdefer { + for (items.items) |item| { + item.deinit(ctx.gpa); + } + items.deinit(); + } + + // Get all exported definitions + const exports_slice = module_env.store.sliceDefs(module_env.exports); + + // If no exports, try all_defs (for modules that are still being processed) + const defs_slice = if (exports_slice.len == 0) + module_env.store.sliceDefs(module_env.all_defs) + else + exports_slice; + + for (defs_slice) |def_idx| { + const def = module_env.store.getDef(def_idx); + + // Get the pattern to find the name + const pattern = module_env.store.getPattern(def.pattern); + + // Extract name from pattern (could be assign, nominal, etc.) + const name_ident_opt = switch (pattern) { + .assign => |a| a.ident, + .nominal => |n| blk: { + // For nominal types, we need to get the statement and extract the header + const stmt = module_env.store.getStatement(n.nominal_type_decl); + break :blk switch (stmt) { + .s_nominal_decl => |decl| module_env.store.getTypeHeader(decl.header).name, + else => continue, + }; + }, + else => continue, + }; + + const name = try ctx.gpa.dupe(u8, module_env.getIdentText(name_ident_opt)); + errdefer ctx.gpa.free(name); + + // Extract nested associated items if this is a nominal type with a record + const children = switch (pattern) { + .nominal => blk: { + // For nominal types, look at the expression to find associated items + const expr = module_env.store.getExpr(def.expr); + break :blk switch (expr) { + .e_nominal => |nom_expr| blk2: { + const backing = module_env.store.getExpr(nom_expr.backing_expr); + break :blk2 switch (backing) { + .e_record => |record| try extractRecordAssociatedItems(ctx, module_env, record.fields), + else => try ctx.gpa.alloc(AssociatedItem, 0), + }; + }, + else => try ctx.gpa.alloc(AssociatedItem, 0), + }; + }, + else => try ctx.gpa.alloc(AssociatedItem, 0), + }; + + try items.append(.{ + .name = name, + .children = children, + }); + } + + return try items.toOwnedSlice(); +} + +/// Generate documentation for the root and all its dependencies and imported modules +fn generateDocs( + ctx: *CliContext, + build_env: *compile.BuildEnv, + module_path: []const u8, + base_output_dir: []const u8, +) !void { + // First, determine if this is an app or other kind + var pkg_iter = build_env.packages.iterator(); + const first_pkg = if (pkg_iter.next()) |entry| entry.value_ptr.* else return; + + const is_app = first_pkg.kind == .app; + + if (is_app) { + // For apps, collect all imported modules and generate sidebar + try generateAppDocs(ctx, build_env, module_path, base_output_dir); + } else { + // For packages, just generate package dependency docs + try generatePackageDocs(ctx, build_env, module_path, base_output_dir, ""); + } +} + +/// Generate docs for an app module +fn generateAppDocs( + ctx: *CliContext, + build_env: *compile.BuildEnv, + module_path: []const u8, + base_output_dir: []const u8, +) !void { + // Collect all imported modules (both local and from packages) + var modules_map = std.StringHashMap(ModuleInfo).init(ctx.gpa); + defer { + var it = modules_map.iterator(); + while (it.next()) |entry| { + entry.value_ptr.deinit(ctx.gpa); + } + modules_map.deinit(); + } + + // Get the root package + var pkg_iter = build_env.packages.iterator(); + const first_pkg = if (pkg_iter.next()) |entry| entry.value_ptr.* else return; + + // Iterate through schedulers to get modules + var sched_iter = build_env.schedulers.iterator(); + while (sched_iter.next()) |sched_entry| { + const package_name = sched_entry.key_ptr.*; + const package_env = sched_entry.value_ptr.*; + + // Iterate through modules in this package + for (package_env.modules.items) |module_state| { + // Process external imports (e.g., "cli.Stdout") + for (module_state.external_imports.items) |ext_import| { + // Parse the import (e.g., "cli.Stdout" -> package="cli", module="Stdout") + if (std.mem.indexOfScalar(u8, ext_import, '.')) |dot_index| { + const pkg_shorthand = ext_import[0..dot_index]; + const module_name = ext_import[dot_index + 1 ..]; + + // Create full name and link path + const full_name = try ctx.arena.dupe(u8, ext_import); + const link_path = try std.fmt.allocPrint(ctx.arena, "{s}/{s}", .{ pkg_shorthand, module_name }); + + const empty_items = [_]AssociatedItem{}; + const mod_info = ModuleInfo{ + .name = full_name, + .link_path = link_path, + .associated_items = &empty_items, + }; + + // Add to map (deduplicates automatically) + const gop = try modules_map.getOrPut(full_name); + if (!gop.found_existing) { + gop.value_ptr.* = mod_info; + } + + // Generate index.html for this module + const module_output_dir = try std.fs.path.join(ctx.arena, &[_][]const u8{ base_output_dir, pkg_shorthand, module_name }); + generateModuleIndex(ctx, module_output_dir, ext_import) catch |err| { + std.debug.print("Warning: failed to generate module index for {s}: {}\n", .{ ext_import, err }); + }; + } + } + + // Process local imports (non-external modules in the same package) + for (module_state.imports.items) |import_id| { + if (import_id < package_env.modules.items.len) { + const imported_module = package_env.modules.items[import_id]; + const module_name = imported_module.name; + + // Skip if this is the root module itself + if (std.mem.eql(u8, module_name, "main")) continue; + + // Only include if it's a local module (not from a package) + if (std.mem.eql(u8, package_name, first_pkg.name)) { + const full_name = try ctx.gpa.dupe(u8, module_name); + const link_path = try ctx.gpa.dupe(u8, module_name); + + // Extract associated items from the module if it has an env + const associated_items = if (imported_module.env) |*mod_env| + try extractAssociatedItems(ctx, mod_env) + else + try ctx.gpa.alloc(AssociatedItem, 0); + + const mod_info = ModuleInfo{ + .name = full_name, + .link_path = link_path, + .associated_items = associated_items, + }; + + const gop = try modules_map.getOrPut(full_name); + if (!gop.found_existing) { + gop.value_ptr.* = mod_info; + } else { + // Free the duplicates + ctx.gpa.free(full_name); + ctx.gpa.free(link_path); + for (associated_items) |item| { + item.deinit(ctx.gpa); + } + ctx.gpa.free(associated_items); + } + + // Generate index.html for this local module + const module_output_dir = try std.fs.path.join(ctx.arena, &[_][]const u8{ base_output_dir, module_name }); + generateModuleIndex(ctx, module_output_dir, module_name) catch |err| { + std.debug.print("Warning: failed to generate module index for {s}: {}\n", .{ module_name, err }); + }; + } + } + } + } + } + + // Convert map to sorted list + var modules_list = std.ArrayList(ModuleInfo).empty; + defer modules_list.deinit(ctx.gpa); + var map_iter = modules_map.iterator(); + while (map_iter.next()) |entry| { + try modules_list.append(ctx.gpa, entry.value_ptr.*); + } + + // Collect package shorthands + var shorthands_list = std.array_list.Managed([]const u8).init(ctx.gpa); + defer { + for (shorthands_list.items) |item| ctx.gpa.free(item); + shorthands_list.deinit(); + } + + var shorthand_iter = first_pkg.shorthands.iterator(); + while (shorthand_iter.next()) |sh_entry| { + const shorthand = try ctx.gpa.dupe(u8, sh_entry.key_ptr.*); + try shorthands_list.append(shorthand); + } + + // Generate root index.html + try generatePackageIndex(ctx, base_output_dir, module_path, shorthands_list.items, modules_list.items); + + // Generate package dependency docs recursively + shorthand_iter = first_pkg.shorthands.iterator(); + while (shorthand_iter.next()) |sh_entry| { + const shorthand = sh_entry.key_ptr.*; + const dep_ref = sh_entry.value_ptr.*; + + generatePackageDocs(ctx, build_env, dep_ref.root_file, base_output_dir, shorthand) catch |err| { + std.debug.print("Warning: failed to generate docs for package {s}: {}\n", .{ shorthand, err }); + }; + } +} + +/// Recursively generate documentation for a package and its dependencies +fn generatePackageDocs( + ctx: *CliContext, + build_env: *compile.BuildEnv, + module_path: []const u8, + base_output_dir: []const u8, + relative_path: []const u8, +) error{OutOfMemory}!void { + const output_dir = if (relative_path.len == 0) + try ctx.arena.dupe(u8, base_output_dir) + else + try std.fs.path.join(ctx.arena, &[_][]const u8{ base_output_dir, relative_path }); + + var shorthands_list = std.array_list.Managed([]const u8).init(ctx.gpa); + defer { + for (shorthands_list.items) |item| ctx.gpa.free(item); + shorthands_list.deinit(); + } + + var pkg_iter = build_env.packages.iterator(); + while (pkg_iter.next()) |entry| { + const pkg = entry.value_ptr; + + var shorthand_iter = pkg.shorthands.iterator(); + while (shorthand_iter.next()) |sh_entry| { + const shorthand = try ctx.gpa.dupe(u8, sh_entry.key_ptr.*); + try shorthands_list.append(shorthand); + } + + shorthand_iter = pkg.shorthands.iterator(); + while (shorthand_iter.next()) |sh_entry| { + const shorthand = sh_entry.key_ptr.*; + + const dep_relative_path = if (relative_path.len == 0) + try ctx.arena.dupe(u8, shorthand) + else + try std.fs.path.join(ctx.arena, &[_][]const u8{ relative_path, shorthand }); + + const dep_ref = sh_entry.value_ptr.*; + generatePackageDocs(ctx, build_env, dep_ref.root_file, base_output_dir, dep_relative_path) catch |err| { + std.debug.print("Warning: failed to generate docs for {s}: {}\n", .{ shorthand, err }); + }; + } + + break; + } + + // For standalone modules, extract and display their exports + var module_infos = std.array_list.Managed(ModuleInfo).init(ctx.gpa); + defer { + for (module_infos.items) |mod| mod.deinit(ctx.gpa); + module_infos.deinit(); + } + + // Get the module's exports if it's a standalone module + var sched_iter = build_env.schedulers.iterator(); + while (sched_iter.next()) |sched_entry| { + const package_env = sched_entry.value_ptr.*; + + // Check ALL modules in this package + for (package_env.modules.items) |module_state| { + if (module_state.env) |*mod_env| { + const associated_items = try extractAssociatedItems(ctx, mod_env); + const mod_name = try ctx.gpa.dupe(u8, module_state.name); + + try module_infos.append(.{ + .name = mod_name, + .link_path = try ctx.gpa.dupe(u8, ""), + .associated_items = associated_items, + }); + } + } + } + + generatePackageIndex(ctx, output_dir, module_path, shorthands_list.items, module_infos.items) catch |err| { + std.debug.print("Warning: failed to generate index for {s}: {}\n", .{ module_path, err }); + }; +} + +test "appendWindowsQuotedArg" { + const testing = std.testing; + + // Helper to test the quoting function + const testQuote = struct { + fn run(input: []const u8, expected: []const u8) !void { + var cmd = std.array_list.Managed(u8).initCapacity(testing.allocator, 64) catch unreachable; + defer cmd.deinit(); + try appendWindowsQuotedArg(&cmd, input); + try testing.expectEqualStrings(expected, cmd.items); + } + }.run; + + // Simple arg without spaces - no quoting needed + try testQuote("simple", "simple"); + + // Arg with spaces - needs quoting + try testQuote("hello world", "\"hello world\""); + + // Arg with tab - needs quoting + try testQuote("hello\tworld", "\"hello\tworld\""); + + // Empty arg - needs quoting + try testQuote("", "\"\""); + + // Arg with embedded quote - needs escaping + try testQuote("say \"hello\"", "\"say \\\"hello\\\"\""); + + // Arg with backslash not before quote - unchanged + try testQuote("path\\to\\file", "path\\to\\file"); + + // Arg with backslash before quote - backslash doubled + try testQuote("path\\\"quote", "\"path\\\\\\\"quote\""); + + // Arg with trailing backslash - doubled when quoted + try testQuote("path with spaces\\", "\"path with spaces\\\\\""); + + // Arg with multiple trailing backslashes (needs space to trigger quoting) + try testQuote("has spaces\\\\", "\"has spaces\\\\\\\\\""); } diff --git a/src/cli/platform_host_shim.zig b/src/cli/platform_host_shim.zig new file mode 100644 index 0000000000..0b52c5302b --- /dev/null +++ b/src/cli/platform_host_shim.zig @@ -0,0 +1,248 @@ +//! Helpers for using Zig's LLVM Builder API to generate a shim library for the +//! Roc interpreter that translates from the platform host API. +//! +//! Note: Symbol names in LLVM IR need platform-specific prefixes for macOS. +//! MachO format requires underscore prefix on all C symbols. + +const std = @import("std"); +const Builder = std.zig.llvm.Builder; +const WipFunction = Builder.WipFunction; +const RocTarget = @import("target.zig").RocTarget; + +/// Represents a single entrypoint that a Roc platform host expects to call. +/// Each entrypoint corresponds to a specific function the host can invoke, +/// such as "init", "render", "update", etc. +pub const EntryPoint = struct { + /// The name of the entrypoint function (without the "roc__" prefix). + /// This will be used to generate the exported function name. + /// For example, "init" becomes "roc__init". + name: []const u8, + + /// The unique index for this entrypoint that gets passed to roc_entrypoint. + /// This allows the Roc runtime to dispatch to the correct implementation + /// based on which exported function was called by the host. + idx: u32, +}; + +/// Adds the extern declaration for `roc_entrypoint` to the LLVM module. +/// +/// This function creates the declaration for the single entry point that all +/// Roc platform functions will delegate to. The Roc interpreter provides +/// the actual implementation of this function, which acts as a dispatcher +/// based on the entry_idx parameter. +fn addRocEntrypoint(builder: *Builder, target: RocTarget) !Builder.Function.Index { + // For wasm32, use i32 explicitly for pointer parameters (wasm32 C ABI uses 32-bit pointers) + // For other targets, use opaque pointer type which LLVM sizes based on target + const ptr_type: Builder.Type = if (target == .wasm32) .i32 else try builder.ptrType(.default); + + // Create the roc_entrypoint function type: + // void roc_entrypoint(u32 entry_idx, RocOps* ops, void* ret_ptr, void* arg_ptr) + const entrypoint_params = [_]Builder.Type{ .i32, ptr_type, ptr_type, ptr_type }; + const entrypoint_type = try builder.fnType(.void, &entrypoint_params, .normal); + + // Add underscore prefix for macOS (required for MachO symbol names) + const base_name = "roc_entrypoint"; + const full_name = if (target.isMacOS()) + try std.fmt.allocPrint(builder.gpa, "_{s}", .{base_name}) + else + try builder.gpa.dupe(u8, base_name); + defer builder.gpa.free(full_name); + const fn_name = try builder.strtabString(full_name); + + // Add the extern function declaration (no body) + const entrypoint_fn = try builder.addFunction(entrypoint_type, fn_name, .default); + entrypoint_fn.setLinkage(.external, builder); + + return entrypoint_fn; +} + +/// Generates a single exported platform function that delegates to roc_entrypoint. +/// +/// This creates the "glue" functions that a Roc platform host expects to find when +/// linking against a Roc application. Each generated function follows the exact +/// Roc Host ABI specification and simply forwards the call to the interpreter's `roc_entrypoint` +/// with the appropriate index. +/// +/// For example, if name="render" and entry_idx=1, this generates: +/// ```llvm +/// define void @roc__render(ptr %ops, ptr %ret_ptr, ptr %arg_ptr) { +/// call void @roc_entrypoint(i32 1, ptr %ops, ptr %ret_ptr, ptr %arg_ptr) +/// ret void +/// } +/// ``` +/// +/// This delegation pattern allows: +/// 1. The host to call specific named functions (roc__init, roc__render, etc.) +/// 2. The pre-built Roc interpreter to handle all calls through a single dispatch mechanism +/// 3. Efficient code generation since each wrapper is just a simple function call +/// 4. Easy addition/removal of platform functions without changing the pre-built interpreter binary which is embedded in the roc cli executable. +fn addRocExportedFunction(builder: *Builder, entrypoint_fn: Builder.Function.Index, name: []const u8, entry_idx: u32, target: RocTarget) !Builder.Function.Index { + // For wasm32, use i32 explicitly for pointer parameters (wasm32 C ABI uses 32-bit pointers) + // For other targets, use opaque pointer type which LLVM sizes based on target + const ptr_type: Builder.Type = if (target == .wasm32) .i32 else try builder.ptrType(.default); + + // Create the Roc function type following the ABI: + // void roc_function(RocOps* ops, void* ret_ptr, void* arg_ptr) + const roc_fn_params = [_]Builder.Type{ ptr_type, ptr_type, ptr_type }; + const roc_fn_type = try builder.fnType(.void, &roc_fn_params, .normal); + + // Create function name with roc__ prefix. + // Add underscore prefix for macOS (required for MachO symbol names) + const base_name = try std.fmt.allocPrint(builder.gpa, "roc__{s}", .{name}); + defer builder.gpa.free(base_name); + const full_name = if (target.isMacOS()) + try std.fmt.allocPrint(builder.gpa, "_{s}", .{base_name}) + else + try builder.gpa.dupe(u8, base_name); + defer builder.gpa.free(full_name); + const fn_name = try builder.strtabString(full_name); + + // Add the function to the module with external linkage + const roc_fn = try builder.addFunction(roc_fn_type, fn_name, .default); + roc_fn.setLinkage(.external, builder); + + // Create a work-in-progress function to add instructions + var wip = try WipFunction.init(builder, .{ + .function = roc_fn, + .strip = false, + }); + defer wip.deinit(); + + // Create the entry basic block + const entry_block = try wip.block(0, "entry"); + wip.cursor = .{ .block = entry_block }; + + // Get the function parameters + const ops_ptr = wip.arg(0); // RocOps pointer + const ret_ptr = wip.arg(1); // Return value pointer + const arg_ptr = wip.arg(2); // Arguments pointer + + // Create constant for entry_idx + const idx_const = try builder.intConst(.i32, entry_idx); + + // Call roc_entrypoint(entry_idx, ops, ret_ptr, arg_ptr) + const call_args = [_]Builder.Value{ idx_const.toValue(), ops_ptr, ret_ptr, arg_ptr }; + _ = try wip.call(.normal, .ccc, .none, entrypoint_fn.typeOf(builder), entrypoint_fn.toValue(builder), &call_args, ""); + + // Return void + _ = try wip.retVoid(); + + // Finish building the function + try wip.finish(); + + return roc_fn; +} + +/// Creates a complete Roc platform library with all necessary entrypoints. +/// +/// This generates a shim that translates between the pre-built roc interpreter +/// which has a single `roc_entrypoint`, and the API defined by the platform with the +/// specific entrypoints the host expects to link with. +/// +/// The generated library structure follows this pattern: +/// ```llvm +/// ; External function that provided by the pre-built roc interpreter +/// declare void @roc_entrypoint(i32 %entry_idx, ptr %ops, ptr %ret_ptr, ptr %arg_ptr) +/// +/// ; Platform functions that the host expects to be linked with +/// define void @roc__init(ptr %ops, ptr %ret_ptr, ptr %arg_ptr) { +/// call void @roc_entrypoint(i32 0, ptr %ops, ptr %ret_ptr, ptr %arg_ptr) +/// ret void +/// } +/// +/// define void @roc__render(ptr %ops, ptr %ret_ptr, ptr %arg_ptr) { +/// call void @roc_entrypoint(i32 1, ptr %ops, ptr %ret_ptr, ptr %arg_ptr) +/// ret void +/// } +/// ; ... etc for each entrypoint +/// ``` +/// +/// The generated library is then compiled using LLVM to an object file and linked with +/// both the host and the Roc interpreter to create a dev build executable. +pub fn createInterpreterShim(builder: *Builder, entrypoints: []const EntryPoint, target: RocTarget, serialized_module: ?[]const u8) !void { + // Add the extern roc_entrypoint declaration + const entrypoint_fn = try addRocEntrypoint(builder, target); + + // Add each exported entrypoint function + for (entrypoints) |entry| { + _ = try addRocExportedFunction(builder, entrypoint_fn, entry.name, entry.idx, target); + } + + try addRocSerializedModule(builder, target, serialized_module); +} + +/// Adds exported globals for serialized module data. +/// +/// This creates two exported globals: +/// - roc__serialized_base_ptr: pointer to the serialized data (or null) +/// - roc__serialized_size: size of the serialized data in bytes (or 0) +/// +/// When data is provided, an internal constant array is created and the base_ptr +/// points to it. When data is null, both values are set to null/zero. +fn addRocSerializedModule(builder: *Builder, target: RocTarget, serialized_module: ?[]const u8) !void { + // Use opaque pointer type for globals - LLVM sizes them correctly based on target data layout + const ptr_type = try builder.ptrType(.default); + + // Determine usize type based on target pointer width + const usize_type: Builder.Type = switch (target.ptrBitWidth()) { + 32 => .i32, + 64 => .i64, + else => unreachable, + }; + + // Create platform-specific name for base_ptr + // Add underscore prefix for macOS (required for MachO symbol names) + const base_ptr_name_str = if (target.isMacOS()) + try std.fmt.allocPrint(builder.gpa, "_roc__serialized_base_ptr", .{}) + else + try builder.gpa.dupe(u8, "roc__serialized_base_ptr"); + defer builder.gpa.free(base_ptr_name_str); + const base_ptr_name = try builder.strtabString(base_ptr_name_str); + + // Create platform-specific name for size + const size_name_str = if (target.isMacOS()) + try std.fmt.allocPrint(builder.gpa, "_roc__serialized_size", .{}) + else + try builder.gpa.dupe(u8, "roc__serialized_size"); + defer builder.gpa.free(size_name_str); + const size_name = try builder.strtabString(size_name_str); + + if (serialized_module) |bytes| { + // Create a string constant for the byte data + const str = try builder.string(bytes); + const str_const = try builder.stringConst(str); + + // Create an internal constant variable to hold the array + // IMPORTANT: Set 8-byte alignment to ensure ModuleEnv.Serialized can be accessed properly + // (ModuleEnv.Serialized contains u64/i64 fields that require 8-byte alignment) + const internal_name = try builder.strtabString(".roc_serialized_data"); + const array_var = try builder.addVariable(internal_name, str_const.typeOf(builder), .default); + try array_var.setInitializer(str_const, builder); + array_var.setLinkage(.internal, builder); + array_var.setMutability(.global, builder); + array_var.setAlignment(Builder.Alignment.fromByteUnits(8), builder); + + // Create the external base_ptr variable pointing to the internal array + const base_ptr_var = try builder.addVariable(base_ptr_name, ptr_type, .default); + try base_ptr_var.setInitializer(array_var.toConst(builder), builder); + base_ptr_var.setLinkage(.external, builder); + + // Create the external size variable + const size_const = try builder.intConst(usize_type, bytes.len); + const size_var = try builder.addVariable(size_name, usize_type, .default); + try size_var.setInitializer(size_const, builder); + size_var.setLinkage(.external, builder); + } else { + // Create null pointer for base_ptr + const null_ptr = try builder.nullConst(ptr_type); + const base_ptr_var = try builder.addVariable(base_ptr_name, ptr_type, .default); + try base_ptr_var.setInitializer(null_ptr, builder); + base_ptr_var.setLinkage(.external, builder); + + // Create zero size + const zero_size = try builder.intConst(usize_type, 0); + const size_var = try builder.addVariable(size_name, usize_type, .default); + try size_var.setInitializer(zero_size, builder); + size_var.setLinkage(.external, builder); + } +} diff --git a/src/cli/platform_validation.zig b/src/cli/platform_validation.zig new file mode 100644 index 0000000000..7523a12bbe --- /dev/null +++ b/src/cli/platform_validation.zig @@ -0,0 +1,348 @@ +//! Platform header validation utilities. +//! +//! Provides shared validation logic for platform headers, including: +//! - Parsing platform headers to extract TargetsConfig +//! - Validating targets section exists +//! - Validating target files exist on disk +//! - Validating a specific target is supported +//! +//! This module is used by both `roc build` and `roc bundle` commands. + +const std = @import("std"); +const builtin = @import("builtin"); +const parse = @import("parse"); +const base = @import("base"); +const reporting = @import("reporting"); +const target_mod = @import("target.zig"); +pub const targets_validator = @import("targets_validator.zig"); + +const TargetsConfig = target_mod.TargetsConfig; +const RocTarget = target_mod.RocTarget; +const LinkType = target_mod.LinkType; +const LinkItem = target_mod.LinkItem; +const TargetLinkSpec = target_mod.TargetLinkSpec; + +const is_windows = builtin.target.os.tag == .windows; + +var stderr_file_writer: std.fs.File.Writer = .{ + .interface = std.fs.File.Writer.initInterface(&.{}), + .file = if (is_windows) undefined else std.fs.File.stderr(), + .mode = .streaming, +}; + +fn stderrWriter() *std.Io.Writer { + if (is_windows) stderr_file_writer.file = std.fs.File.stderr(); + return &stderr_file_writer.interface; +} + +/// Re-export ValidationResult for callers that need to create reports +pub const ValidationResult = targets_validator.ValidationResult; + +/// Errors that can occur during platform validation +pub const ValidationError = error{ + /// Platform header is missing required targets section + MissingTargetsSection, + /// Requested target is not declared in platform's targets section + UnsupportedTarget, + /// A file declared in targets section doesn't exist + MissingTargetFile, + /// Files directory specified in targets section doesn't exist + MissingFilesDirectory, + /// Failed to parse platform header + ParseError, + /// Failed to read platform source file + FileReadError, + /// Out of memory + OutOfMemory, +}; + +/// Result of platform validation with parsed config +pub const PlatformValidation = struct { + /// Parsed targets configuration + config: TargetsConfig, + /// Directory containing the platform (dirname of platform source) + platform_dir: []const u8, +}; + +/// Check if a file is a platform header (has `platform` at the start). +/// This is a quick check without side effects - useful for finding which file +/// in a set of .roc files is the actual platform file. +/// Returns true if the file is a platform, false if it's a module/app, or null on error. +pub fn isPlatformFile( + allocator: std.mem.Allocator, + source_path: []const u8, +) ?bool { + // Read source file + var source = std.fs.cwd().readFileAlloc(allocator, source_path, std.math.maxInt(usize)) catch { + return null; + }; + source = base.source_utils.normalizeLineEndingsRealloc(allocator, source) catch { + allocator.free(source); + return null; + }; + defer allocator.free(source); + + // Initialize parse environment + var env = base.CommonEnv.init(allocator, source) catch { + return null; + }; + + // Parse the file + const ast = parse.parse(&env, allocator) catch { + return null; + }; + + // Check the header type + const file = ast.store.getFile(); + const header = ast.store.getHeader(file.header); + + return switch (header) { + .platform => true, + else => false, + }; +} + +/// Parse and validate a platform header. +/// Returns the TargetsConfig if valid, or an error with details. +pub fn validatePlatformHeader( + allocator: std.mem.Allocator, + platform_source_path: []const u8, +) ValidationError!PlatformValidation { + // Read platform source + var source = std.fs.cwd().readFileAlloc(allocator, platform_source_path, std.math.maxInt(usize)) catch { + renderFileReadError(allocator, platform_source_path); + return error.FileReadError; + }; + source = base.source_utils.normalizeLineEndingsRealloc(allocator, source) catch { + allocator.free(source); + return error.OutOfMemory; + }; + + // Parse platform header + var env = base.CommonEnv.init(allocator, source) catch { + std.log.err("Failed to initialize parse environment for: {s}", .{platform_source_path}); + return error.ParseError; + }; + + const ast = parse.parse(&env, allocator) catch { + renderParseError(allocator, platform_source_path); + return error.ParseError; + }; + + // Extract TargetsConfig + const config = TargetsConfig.fromAST(allocator, ast) catch { + return error.ParseError; + } orelse { + renderMissingTargetsError(allocator, platform_source_path); + return error.MissingTargetsSection; + }; + + return .{ + .config = config, + .platform_dir = std.fs.path.dirname(platform_source_path) orelse ".", + }; +} + +/// Render a file read error report to stderr. +fn renderFileReadError(allocator: std.mem.Allocator, path: []const u8) void { + var report = reporting.Report.init(allocator, "FILE READ ERROR", .fatal); + defer report.deinit(); + + report.document.addText("Failed to read platform source file:") catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText(" ") catch return; + report.document.addAnnotated(path, .path) catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText("Check that the file exists and you have read permissions.") catch return; + report.document.addLineBreak() catch return; + + reporting.renderReportToTerminal( + &report, + stderrWriter(), + .ANSI, + reporting.ReportingConfig.initColorTerminal(), + ) catch {}; +} + +/// Render a parse error report to stderr. +fn renderParseError(allocator: std.mem.Allocator, path: []const u8) void { + var report = reporting.Report.init(allocator, "PARSE ERROR", .fatal); + defer report.deinit(); + + report.document.addText("Failed to parse platform header:") catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText(" ") catch return; + report.document.addAnnotated(path, .path) catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText("Check that the file contains valid Roc syntax.") catch return; + report.document.addLineBreak() catch return; + + reporting.renderReportToTerminal( + &report, + stderrWriter(), + .ANSI, + reporting.ReportingConfig.initColorTerminal(), + ) catch {}; +} + +/// Render a missing targets section error report to stderr. +fn renderMissingTargetsError(allocator: std.mem.Allocator, path: []const u8) void { + var report = reporting.Report.init(allocator, "MISSING TARGETS SECTION", .fatal); + defer report.deinit(); + + report.document.addText("Platform at ") catch return; + report.document.addAnnotated(path, .path) catch return; + report.document.addText(" does not have a 'targets:' section.") catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addText("Platform headers must declare supported targets. Example:") catch return; + report.document.addLineBreak() catch return; + report.document.addLineBreak() catch return; + report.document.addCodeBlock( + \\ targets: { + \\ files: "targets/", + \\ exe: { + \\ x64linux: ["host.o", app], + \\ arm64linux: ["host.o", app], + \\ } + \\ } + ) catch return; + report.document.addLineBreak() catch return; + + reporting.renderReportToTerminal( + &report, + stderrWriter(), + .ANSI, + reporting.ReportingConfig.initColorTerminal(), + ) catch {}; +} + +/// Validate that a specific target is supported by the platform. +/// Returns error.UnsupportedTarget if the target is not in the config. +/// Does not log - caller should handle error reporting. +pub fn validateTargetSupported( + config: TargetsConfig, + target: RocTarget, + link_type: LinkType, +) ValidationError!void { + if (!config.supportsTarget(target, link_type)) { + return error.UnsupportedTarget; + } +} + +/// Create a ValidationResult for an unsupported target error. +/// This can be passed to targets_validator.createValidationReport for nice error formatting. +pub fn createUnsupportedTargetResult( + platform_path: []const u8, + requested_target: RocTarget, + link_type: LinkType, + config: TargetsConfig, +) ValidationResult { + return .{ + .unsupported_target = .{ + .platform_path = platform_path, + .requested_target = requested_target, + .link_type = link_type, + .supported_targets = config.getSupportedTargets(link_type), + }, + }; +} + +/// Render a validation error to stderr using the reporting infrastructure. +/// Returns true if a report was rendered, false if no report was needed. +pub fn renderValidationError( + allocator: std.mem.Allocator, + result: ValidationResult, + stderr: anytype, +) bool { + switch (result) { + .valid => return false, + else => { + var report = targets_validator.createValidationReport(allocator, result) catch { + // Fallback to simple logging if report creation fails + std.log.err("Platform validation failed", .{}); + return true; + }; + defer report.deinit(); + + reporting.renderReportToTerminal( + &report, + stderr, + .ANSI, + reporting.ReportingConfig.initColorTerminal(), + ) catch {}; + return true; + }, + } +} + +/// Validate all files declared in targets section exist on disk. +/// Uses existing targets_validator infrastructure. +/// Returns the ValidationResult for nice error reporting, or null if validation passed. +pub fn validateAllTargetFilesExist( + allocator: std.mem.Allocator, + config: TargetsConfig, + platform_dir_path: []const u8, +) ?ValidationResult { + var platform_dir = std.fs.cwd().openDir(platform_dir_path, .{}) catch { + return .{ + .missing_files_directory = .{ + .platform_path = platform_dir_path, + .files_dir = config.files_dir orelse "targets", + }, + }; + }; + defer platform_dir.close(); + + const result = targets_validator.validateTargetFilesExist(allocator, config, platform_dir) catch { + return .{ + .missing_files_directory = .{ + .platform_path = platform_dir_path, + .files_dir = config.files_dir orelse "targets", + }, + }; + }; + + switch (result) { + .valid => return null, + else => return result, + } +} + +// Tests +const testing = std.testing; + +test "validateTargetSupported returns error for unsupported target" { + const config = TargetsConfig{ + .files_dir = "targets", + .exe = &.{ + .{ .target = .x64mac, .items = &.{.app} }, + .{ .target = .arm64mac, .items = &.{.app} }, + }, + .static_lib = &.{}, + .shared_lib = &.{}, + }; + + // x64musl is not in the config, should error + const result = validateTargetSupported(config, .x64musl, .exe); + try testing.expectError(error.UnsupportedTarget, result); +} + +test "validateTargetSupported succeeds for supported target" { + const config = TargetsConfig{ + .files_dir = "targets", + .exe = &.{ + .{ .target = .x64mac, .items = &.{.app} }, + .{ .target = .arm64mac, .items = &.{.app} }, + }, + .static_lib = &.{}, + .shared_lib = &.{}, + }; + + // x64mac is in the config, should succeed + try validateTargetSupported(config, .x64mac, .exe); +} diff --git a/src/cli/target.zig b/src/cli/target.zig new file mode 100644 index 0000000000..32314fe67a --- /dev/null +++ b/src/cli/target.zig @@ -0,0 +1,265 @@ +//! Roc target definitions and link configuration +//! +//! Re-exports RocTarget and adds link configuration types that depend on the parse module. + +const std = @import("std"); +const parse = @import("parse"); + +const Allocator = std.mem.Allocator; + +// Re-export RocTarget from the shared build module +pub const RocTarget = @import("roc_target").RocTarget; + +/// Individual link item from a targets section +/// Can be a file path (relative to files/ directory) or a special identifier +pub const LinkItem = union(enum) { + /// A file path (string literal in the source) + /// Path is relative to the targets// directory + file_path: []const u8, + + /// The compiled Roc application + app, + + /// Windows GUI subsystem flag (/subsystem:windows) + win_gui, +}; + +/// Link specification for a single target +/// Contains the ordered list of items to link for this target +pub const TargetLinkSpec = struct { + target: RocTarget, + items: []const LinkItem, +}; + +/// Type of output binary +pub const LinkType = enum { + /// Executable binary + exe, + /// Static library (.a, .lib) + static_lib, + /// Shared/dynamic library (.so, .dylib, .dll) + shared_lib, +}; + +/// Complete targets configuration from a platform header +pub const TargetsConfig = struct { + /// Base directory for target-specific files (e.g., "targets/") + files_dir: ?[]const u8, + + /// Executable target specifications (in priority order) + exe: []const TargetLinkSpec, + + /// Static library target specifications (in priority order) + static_lib: []const TargetLinkSpec, + + /// Shared library target specifications (in priority order) + shared_lib: []const TargetLinkSpec, + + /// Get the link spec for a specific target and link type + pub fn getLinkSpec(self: TargetsConfig, target: RocTarget, link_type: LinkType) ?TargetLinkSpec { + const specs = switch (link_type) { + .exe => self.exe, + .static_lib => self.static_lib, + .shared_lib => self.shared_lib, + }; + for (specs) |spec| { + if (spec.target == target) { + return spec; + } + } + return null; + } + + /// Get the default target for a given link type based on the current system + /// Returns the first target in the list that's compatible with the current host (OS and arch) + pub fn getDefaultTarget(self: TargetsConfig, link_type: LinkType) ?RocTarget { + const specs = switch (link_type) { + .exe => self.exe, + .static_lib => self.static_lib, + .shared_lib => self.shared_lib, + }; + + // First pass: look for exact OS and architecture match + for (specs) |spec| { + if (spec.target.isCompatibleWithHost()) { + return spec.target; + } + } + + return null; + } + + /// Result of finding a compatible target + pub const CompatibleTarget = struct { + target: RocTarget, + link_type: LinkType, + }; + + /// Get the first compatible target across all link types. + /// Iterates through exe, static_lib, shared_lib in order, + /// returning the first target compatible with the current host. + pub fn getFirstCompatibleTarget(self: TargetsConfig) ?CompatibleTarget { + const link_types = [_]LinkType{ .exe, .static_lib, .shared_lib }; + + for (link_types) |lt| { + const specs = self.getSupportedTargets(lt); + for (specs) |spec| { + if (spec.target.isCompatibleWithHost()) { + return CompatibleTarget{ .target = spec.target, .link_type = lt }; + } + } + } + + return null; + } + + /// Check if a specific target is supported + pub fn supportsTarget(self: TargetsConfig, target: RocTarget, link_type: LinkType) bool { + return self.getLinkSpec(target, link_type) != null; + } + + /// Get all supported targets for a link type + pub fn getSupportedTargets(self: TargetsConfig, link_type: LinkType) []const TargetLinkSpec { + return switch (link_type) { + .exe => self.exe, + .static_lib => self.static_lib, + .shared_lib => self.shared_lib, + }; + } + + /// Create a TargetsConfig from a parsed AST + /// Returns null if the platform header has no targets section + pub fn fromAST(allocator: Allocator, ast: anytype) !?TargetsConfig { + const NodeStore = parse.NodeStore; + + const store: *const NodeStore = &ast.store; + + // Get the file node first, then get the header from it + const file = store.getFile(); + const header = store.getHeader(file.header); + + // Only platform headers have targets + const platform = switch (header) { + .platform => |p| p, + else => return null, + }; + + // If no targets section, return null + const targets_section_idx = platform.targets orelse return null; + const targets_section = store.getTargetsSection(targets_section_idx); + + // Extract files_dir from string literal token (StringPart token) + const files_dir: ?[]const u8 = if (targets_section.files_path) |tok_idx| + ast.resolve(tok_idx) + else + null; + + // Convert exe link type + var exe_specs = std.array_list.Managed(TargetLinkSpec).init(allocator); + errdefer exe_specs.deinit(); + + if (targets_section.exe) |exe_idx| { + const link_type = store.getTargetLinkType(exe_idx); + const entry_indices = store.targetEntrySlice(link_type.entries); + + for (entry_indices) |entry_idx| { + const entry = store.getTargetEntry(entry_idx); + + // Parse target name from token + const target_name = ast.resolve(entry.target); + const target = RocTarget.fromString(target_name) orelse continue; // Skip unknown targets + + // Convert files + var link_items = std.array_list.Managed(LinkItem).init(allocator); + errdefer link_items.deinit(); + + const file_indices = store.targetFileSlice(entry.files); + for (file_indices) |file_idx| { + const target_file = store.getTargetFile(file_idx); + + switch (target_file) { + .string_literal => |tok| { + // The tok points to StringPart token containing the path + const path = ast.resolve(tok); + try link_items.append(.{ .file_path = path }); + }, + .special_ident => |tok| { + const ident = ast.resolve(tok); + if (std.mem.eql(u8, ident, "app")) { + try link_items.append(.app); + } else if (std.mem.eql(u8, ident, "win_gui")) { + try link_items.append(.win_gui); + } + // Skip unknown special identifiers + }, + .malformed => continue, // Skip malformed entries + } + } + + try exe_specs.append(.{ + .target = target, + .items = try link_items.toOwnedSlice(), + }); + } + } + + // Convert static_lib link type + var static_lib_specs = std.array_list.Managed(TargetLinkSpec).init(allocator); + errdefer static_lib_specs.deinit(); + + if (targets_section.static_lib) |static_lib_idx| { + const link_type = store.getTargetLinkType(static_lib_idx); + const entry_indices = store.targetEntrySlice(link_type.entries); + + for (entry_indices) |entry_idx| { + const entry = store.getTargetEntry(entry_idx); + + // Parse target name from token + const target_name = ast.resolve(entry.target); + const target = RocTarget.fromString(target_name) orelse continue; // Skip unknown targets + + // Convert files + var link_items = std.array_list.Managed(LinkItem).init(allocator); + errdefer link_items.deinit(); + + const file_indices = store.targetFileSlice(entry.files); + for (file_indices) |file_idx| { + const target_file = store.getTargetFile(file_idx); + + switch (target_file) { + .string_literal => |tok| { + // The tok points to StringPart token containing the path + const path = ast.resolve(tok); + try link_items.append(.{ .file_path = path }); + }, + .special_ident => |tok| { + const ident = ast.resolve(tok); + if (std.mem.eql(u8, ident, "app")) { + try link_items.append(.app); + } else if (std.mem.eql(u8, ident, "win_gui")) { + try link_items.append(.win_gui); + } + // Skip unknown special identifiers + }, + .malformed => continue, // Skip malformed entries + } + } + + try static_lib_specs.append(.{ + .target = target, + .items = try link_items.toOwnedSlice(), + }); + } + } + + // shared_lib to be added later + const empty_specs: []const TargetLinkSpec = &.{}; + + return TargetsConfig{ + .files_dir = files_dir, + .exe = try exe_specs.toOwnedSlice(), + .static_lib = try static_lib_specs.toOwnedSlice(), + .shared_lib = empty_specs, + }; + } +}; diff --git a/src/cli/targets_validator.zig b/src/cli/targets_validator.zig new file mode 100644 index 0000000000..a60381a8c6 --- /dev/null +++ b/src/cli/targets_validator.zig @@ -0,0 +1,940 @@ +//! Validation for platform targets section +//! +//! Validates that: +//! - Platform headers have a targets section (required) +//! - Files declared in the targets section exist in the filesystem +//! - Files in the targets directory match what's declared in the targets section +//! +//! This module is shared between bundle and unbundle operations. + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const parse = @import("parse"); +const target_mod = @import("target.zig"); +const reporting = @import("reporting"); + +const RocTarget = target_mod.RocTarget; +const TargetsConfig = target_mod.TargetsConfig; +const LinkItem = target_mod.LinkItem; +const TargetLinkSpec = target_mod.TargetLinkSpec; +const LinkType = target_mod.LinkType; +const Report = reporting.Report; +const Severity = reporting.Severity; + +/// Errors that can occur during targets validation +pub const ValidationError = error{ + MissingTargetsSection, + MissingFilesDirectory, + MissingTargetFile, + ExtraFileInTargetsDir, + InvalidTargetName, + EmptyTargetsSection, + OutOfMemory, +}; + +/// Result of validating a targets section +pub const ValidationResult = union(enum) { + /// Validation passed + valid: void, + + /// Platform header is missing the required targets section + missing_targets_section: struct { + platform_path: []const u8, + }, + + /// Files directory specified but doesn't exist + missing_files_directory: struct { + platform_path: []const u8, + files_dir: []const u8, + }, + + /// A file declared in targets doesn't exist + missing_target_file: struct { + target: RocTarget, + link_type: LinkType, + file_path: []const u8, + expected_full_path: []const u8, + }, + + /// Extra file found in targets directory that isn't declared + extra_file: struct { + target: RocTarget, + file_path: []const u8, + }, + + /// Targets section exists but has no target entries + empty_targets: struct { + platform_path: []const u8, + }, + + /// Requested target is not supported by this platform + unsupported_target: struct { + platform_path: []const u8, + requested_target: RocTarget, + link_type: LinkType, + supported_targets: []const TargetLinkSpec, + }, + + /// Cross-compilation requested but platform doesn't have host library for target + missing_cross_compile_host: struct { + platform_path: []const u8, + target: RocTarget, + expected_path: []const u8, + files_dir: []const u8, + }, + + /// glibc cross-compilation is not supported on non-Linux hosts + unsupported_glibc_cross: struct { + target: RocTarget, + host_os: []const u8, + }, + + /// App file doesn't have a platform + no_platform_found: struct { + app_path: []const u8, + }, + + /// Invalid target string provided + invalid_target: struct { + target_str: []const u8, + }, + + /// Linker failed to create executable + linker_failed: struct { + reason: []const u8, + }, + + /// Linker not available (LLVM not built) + linker_not_available: void, + + /// Process crashed during execution (Windows) + process_crashed: struct { + exit_code: u32, + is_access_violation: bool, + }, + + /// Process killed by signal (Unix) + process_signaled: struct { + signal: u32, + }, +}; + +/// Validate that a platform has a targets section +pub fn validatePlatformHasTargets( + ast: anytype, + platform_path: []const u8, +) ValidationResult { + const store = &ast.store; + + // Get the file node first, then get the header from it + const file = store.getFile(); + const header = store.getHeader(file.header); + + // Only platform headers should have targets + const platform = switch (header) { + .platform => |p| p, + else => return .{ .valid = {} }, // Non-platform headers don't need targets + }; + + // Check if targets section exists + if (platform.targets == null) { + return .{ .missing_targets_section = .{ + .platform_path = platform_path, + } }; + } + + return .{ .valid = {} }; +} + +/// Validate that files declared in targets section exist on disk +pub fn validateTargetFilesExist( + allocator: Allocator, + targets_config: TargetsConfig, + platform_dir: std.fs.Dir, +) !ValidationResult { + const files_dir_path = targets_config.files_dir orelse return .{ .valid = {} }; + + // Check if files directory exists + var files_dir = platform_dir.openDir(files_dir_path, .{}) catch { + return .{ .missing_files_directory = .{ + .platform_path = "platform", + .files_dir = files_dir_path, + } }; + }; + defer files_dir.close(); + + // Validate exe targets + for (targets_config.exe) |spec| { + if (try validateTargetSpec(allocator, spec, .exe, files_dir)) |result| { + return result; + } + } + + // Validate static_lib targets + for (targets_config.static_lib) |spec| { + if (try validateTargetSpec(allocator, spec, .static_lib, files_dir)) |result| { + return result; + } + } + + // Validate shared_lib targets + for (targets_config.shared_lib) |spec| { + if (try validateTargetSpec(allocator, spec, .shared_lib, files_dir)) |result| { + return result; + } + } + + return .{ .valid = {} }; +} + +fn validateTargetSpec( + allocator: Allocator, + spec: TargetLinkSpec, + link_type: LinkType, + files_dir: std.fs.Dir, +) !?ValidationResult { + // Get target subdirectory name + const target_subdir = @tagName(spec.target); + + // Open target subdirectory + var target_dir = files_dir.openDir(target_subdir, .{}) catch { + // Target directory doesn't exist - this might be okay if there are no file items + var has_files = false; + for (spec.items) |item| { + switch (item) { + .file_path => { + has_files = true; + break; + }, + .app, .win_gui => {}, + } + } + if (has_files) { + const expected_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ "targets", target_subdir }); + defer allocator.free(expected_path); + return .{ .missing_target_file = .{ + .target = spec.target, + .link_type = link_type, + .file_path = target_subdir, + .expected_full_path = expected_path, + } }; + } + return null; + }; + defer target_dir.close(); + + // Check each file item exists + for (spec.items) |item| { + switch (item) { + .file_path => |path| { + // Check if file exists + target_dir.access(path, .{}) catch { + const expected_path = try std.fmt.allocPrint(allocator, "{s}/{s}/{s}", .{ "targets", target_subdir, path }); + return .{ .missing_target_file = .{ + .target = spec.target, + .link_type = link_type, + .file_path = path, + .expected_full_path = expected_path, + } }; + }; + }, + .app, .win_gui => { + // Special identifiers don't need file validation + }, + } + } + + return null; +} + +/// Create an error report for a validation failure +pub fn createValidationReport( + allocator: Allocator, + result: ValidationResult, +) !Report { + switch (result) { + .valid => unreachable, // Should not create report for valid result + + .missing_targets_section => |info| { + var report = Report.init(allocator, "MISSING TARGETS SECTION", .runtime_error); + + try report.document.addText("Platform headers must include a `targets` section that specifies"); + try report.document.addLineBreak(); + try report.document.addText("which targets this platform supports and what files to link."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("In "); + try report.document.addAnnotated(info.platform_path, .emphasized); + try report.document.addText(", add a targets section like:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addCodeBlock( + \\ targets: { + \\ files: "targets/", + \\ exe: { + \\ x64linux: ["host.o", app], + \\ arm64linux: ["host.o", app], + \\ x64mac: ["host.o", app], + \\ arm64mac: ["host.o", app], + \\ } + \\ } + ); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("The targets section declares:"); + try report.document.addLineBreak(); + try report.document.addText(" - `files`: Directory containing target-specific files"); + try report.document.addLineBreak(); + try report.document.addText(" - `exe`: Targets that build executables"); + try report.document.addLineBreak(); + try report.document.addText(" - Each target lists files to link in order, with `app` for the Roc application"); + try report.document.addLineBreak(); + + return report; + }, + + .missing_files_directory => |info| { + var report = Report.init(allocator, "MISSING FILES DIRECTORY", .runtime_error); + + try report.document.addText("The targets section specifies files directory "); + try report.document.addAnnotated(info.files_dir, .emphasized); + try report.document.addLineBreak(); + try report.document.addText("but this directory doesn't exist."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Create the directory structure:"); + try report.document.addLineBreak(); + try report.document.addCodeBlock( + \\ targets/ + \\ x64linux/ + \\ host.o + \\ arm64linux/ + \\ host.o + \\ ... + ); + try report.document.addLineBreak(); + + return report; + }, + + .missing_target_file => |info| { + var report = Report.init(allocator, "MISSING TARGET FILE", .runtime_error); + + try report.document.addText("The targets section declares file "); + try report.document.addAnnotated(info.file_path, .emphasized); + try report.document.addLineBreak(); + try report.document.addText("for target "); + try report.document.addAnnotated(@tagName(info.target), .emphasized); + try report.document.addText(" but this file doesn't exist."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Expected file at: "); + try report.document.addAnnotated(info.expected_full_path, .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Either add the missing file or remove it from the targets section."); + try report.document.addLineBreak(); + + return report; + }, + + .extra_file => |info| { + var report = Report.init(allocator, "EXTRA FILE IN TARGETS", .warning); + + try report.document.addText("Found file "); + try report.document.addAnnotated(info.file_path, .emphasized); + try report.document.addLineBreak(); + try report.document.addText("in target directory for "); + try report.document.addAnnotated(@tagName(info.target), .emphasized); + try report.document.addLineBreak(); + try report.document.addText("but this file isn't declared in the targets section."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("This file will not be included in the bundle."); + try report.document.addLineBreak(); + try report.document.addText("Either add it to the targets section or delete it."); + try report.document.addLineBreak(); + + return report; + }, + + .empty_targets => |info| { + var report = Report.init(allocator, "EMPTY TARGETS SECTION", .runtime_error); + + try report.document.addText("The targets section in "); + try report.document.addAnnotated(info.platform_path, .emphasized); + try report.document.addLineBreak(); + try report.document.addText("doesn't declare any targets."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Add at least one target to the exe, static_lib, or shared_lib section."); + try report.document.addLineBreak(); + + return report; + }, + + .unsupported_target => |info| { + var report = Report.init(allocator, "UNSUPPORTED TARGET", .runtime_error); + + try report.document.addText("The platform at "); + try report.document.addAnnotated(info.platform_path, .emphasized); + try report.document.addLineBreak(); + try report.document.addText("does not support the "); + try report.document.addAnnotated(@tagName(info.requested_target), .emphasized); + try report.document.addText(" target for "); + try report.document.addAnnotated(@tagName(info.link_type), .emphasized); + try report.document.addText(" builds."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + if (info.supported_targets.len > 0) { + try report.document.addText("Supported targets for "); + try report.document.addAnnotated(@tagName(info.link_type), .emphasized); + try report.document.addText(":"); + try report.document.addLineBreak(); + for (info.supported_targets) |spec| { + try report.document.addText(" - "); + try report.document.addAnnotated(@tagName(spec.target), .emphasized); + try report.document.addLineBreak(); + } + try report.document.addLineBreak(); + } else { + try report.document.addText("This platform has no targets configured for "); + try report.document.addAnnotated(@tagName(info.link_type), .emphasized); + try report.document.addText(" builds."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + } + + try report.document.addText("To add support, update the targets section in the platform header."); + try report.document.addLineBreak(); + + return report; + }, + + .missing_cross_compile_host => |info| { + var report = Report.init(allocator, "MISSING HOST LIBRARY FOR CROSS-COMPILATION", .runtime_error); + + try report.document.addText("Cannot cross-compile for "); + try report.document.addAnnotated(@tagName(info.target), .emphasized); + try report.document.addText(": the platform doesn't provide"); + try report.document.addLineBreak(); + try report.document.addText("a pre-built host library for this target."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Expected host library at:"); + try report.document.addLineBreak(); + try report.document.addText(" "); + try report.document.addAnnotated(info.expected_path, .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Platform authors: build your host for this target and place it at:"); + try report.document.addLineBreak(); + try report.document.addText(" /"); + // Trim trailing slash from files_dir for cleaner display + const trimmed_files_dir = std.mem.trimRight(u8, info.files_dir, "/"); + try report.document.addAnnotated(trimmed_files_dir, .emphasized); + try report.document.addText("/"); + try report.document.addAnnotated(@tagName(info.target), .emphasized); + try report.document.addText("/libhost.a"); + try report.document.addLineBreak(); + + return report; + }, + + .unsupported_glibc_cross => |info| { + var report = Report.init(allocator, "GLIBC CROSS-COMPILATION NOT SUPPORTED", .runtime_error); + + try report.document.addText("Cross-compilation to glibc targets ("); + try report.document.addAnnotated(@tagName(info.target), .emphasized); + try report.document.addText(") is not supported on "); + try report.document.addAnnotated(info.host_os, .emphasized); + try report.document.addText("."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("glibc targets require dynamic linking with libc symbols that"); + try report.document.addLineBreak(); + try report.document.addText("are only available on Linux."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Use a statically-linked musl target instead:"); + try report.document.addLineBreak(); + try report.document.addText(" "); + try report.document.addAnnotated("x64musl", .emphasized); + try report.document.addText(" or "); + try report.document.addAnnotated("arm64musl", .emphasized); + try report.document.addLineBreak(); + + return report; + }, + + .no_platform_found => |info| { + var report = Report.init(allocator, "NO PLATFORM FOUND", .runtime_error); + + try report.document.addText("The file "); + try report.document.addAnnotated(info.app_path, .emphasized); + try report.document.addText(" doesn't have a platform."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Every Roc application needs a platform. Add a platform declaration:"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addCodeBlock( + \\app [main!] { pf: platform "../path/to/platform/main.roc" } + ); + try report.document.addLineBreak(); + + return report; + }, + + .invalid_target => |info| { + var report = Report.init(allocator, "INVALID TARGET", .runtime_error); + + try report.document.addText("The target "); + try report.document.addAnnotated(info.target_str, .emphasized); + try report.document.addText(" is not a valid build target."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Valid targets are:"); + try report.document.addLineBreak(); + try report.document.addText(" x64musl, arm64musl - Linux (static, portable)"); + try report.document.addLineBreak(); + try report.document.addText(" x64glibc, arm64glibc - Linux (dynamic, faster)"); + try report.document.addLineBreak(); + try report.document.addText(" x64mac, arm64mac - macOS"); + try report.document.addLineBreak(); + try report.document.addText(" x64win, arm64win - Windows"); + try report.document.addLineBreak(); + + return report; + }, + + .linker_failed => |info| { + var report = Report.init(allocator, "LINKER FAILED", .runtime_error); + + try report.document.addText("Failed to create executable: "); + try report.document.addAnnotated(info.reason, .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("This may indicate:"); + try report.document.addLineBreak(); + try report.document.addText(" - Missing platform host library (libhost.a)"); + try report.document.addLineBreak(); + try report.document.addText(" - Incompatible object files for the target"); + try report.document.addLineBreak(); + try report.document.addText(" - Missing system libraries"); + try report.document.addLineBreak(); + + return report; + }, + + .linker_not_available => { + var report = Report.init(allocator, "LINKER NOT AVAILABLE", .runtime_error); + + try report.document.addText("The LLD linker is not available."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("This typically occurs when running a test executable"); + try report.document.addLineBreak(); + try report.document.addText("that was built without LLVM support."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("To fix this, rebuild with LLVM enabled."); + try report.document.addLineBreak(); + + return report; + }, + + .process_crashed => |info| { + var report = Report.init(allocator, "PROCESS CRASHED", .runtime_error); + + if (info.is_access_violation) { + try report.document.addText("The program crashed with an access violation (segmentation fault)."); + } else { + var buf: [32]u8 = undefined; + const code_str = std.fmt.bufPrint(&buf, "0x{X}", .{info.exit_code}) catch "unknown"; + + try report.document.addText("The program crashed with exception code: "); + try report.document.addAnnotated(code_str, .emphasized); + } + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("This is likely a bug in the Roc compiler."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Please report this issue at:"); + try report.document.addLineBreak(); + try report.document.addText(" "); + try report.document.addAnnotated("https://github.com/roc-lang/roc/issues", .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Include a small reproduction of the code that causes this crash."); + try report.document.addLineBreak(); + + return report; + }, + + .process_signaled => |info| { + var report = Report.init(allocator, "PROCESS KILLED BY SIGNAL", .runtime_error); + + const signal_name: []const u8 = switch (info.signal) { + 11 => "SIGSEGV (Segmentation fault)", + 6 => "SIGABRT (Aborted)", + 9 => "SIGKILL (Killed)", + 8 => "SIGFPE (Floating point exception)", + 4 => "SIGILL (Illegal instruction)", + 7 => "SIGBUS (Bus error)", + else => "Unknown signal", + }; + + try report.document.addText("The program was killed by signal "); + var buf: [8]u8 = undefined; + const sig_str = std.fmt.bufPrint(&buf, "{d}", .{info.signal}) catch "?"; + try report.document.addAnnotated(sig_str, .emphasized); + try report.document.addText(": "); + try report.document.addAnnotated(signal_name, .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("This is likely a bug in the Roc compiler."); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Please report this issue at:"); + try report.document.addLineBreak(); + try report.document.addText(" "); + try report.document.addAnnotated("https://github.com/roc-lang/roc/issues", .emphasized); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("Include a small reproduction of the code that causes this crash."); + try report.document.addLineBreak(); + + return report; + }, + } +} + +test "createValidationReport generates correct report for missing_targets_section" { + const allocator = std.testing.allocator; + + var report = try createValidationReport(allocator, .{ + .missing_targets_section = .{ .platform_path = "test/platform/main.roc" }, + }); + defer report.deinit(); + + try std.testing.expectEqualStrings("MISSING TARGETS SECTION", report.title); + try std.testing.expectEqual(Severity.runtime_error, report.severity); +} + +test "createValidationReport generates correct report for missing_files_directory" { + const allocator = std.testing.allocator; + + var report = try createValidationReport(allocator, .{ + .missing_files_directory = .{ + .platform_path = "test/platform/main.roc", + .files_dir = "targets/", + }, + }); + defer report.deinit(); + + try std.testing.expectEqualStrings("MISSING FILES DIRECTORY", report.title); + try std.testing.expectEqual(Severity.runtime_error, report.severity); +} + +test "createValidationReport generates correct report for missing_target_file" { + const allocator = std.testing.allocator; + + var report = try createValidationReport(allocator, .{ + .missing_target_file = .{ + .target = .x64linux, + .link_type = .exe, + .file_path = "host.o", + .expected_full_path = "targets/x64linux/host.o", + }, + }); + defer report.deinit(); + + try std.testing.expectEqualStrings("MISSING TARGET FILE", report.title); + try std.testing.expectEqual(Severity.runtime_error, report.severity); +} + +test "createValidationReport generates correct report for extra_file" { + const allocator = std.testing.allocator; + + var report = try createValidationReport(allocator, .{ + .extra_file = .{ + .target = .x64linux, + .file_path = "unused.o", + }, + }); + defer report.deinit(); + + try std.testing.expectEqualStrings("EXTRA FILE IN TARGETS", report.title); + try std.testing.expectEqual(Severity.warning, report.severity); +} + +test "createValidationReport generates correct report for empty_targets" { + const allocator = std.testing.allocator; + + var report = try createValidationReport(allocator, .{ + .empty_targets = .{ .platform_path = "test/platform/main.roc" }, + }); + defer report.deinit(); + + try std.testing.expectEqualStrings("EMPTY TARGETS SECTION", report.title); + try std.testing.expectEqual(Severity.runtime_error, report.severity); +} + +test "validateTargetFilesExist returns valid when no files_dir specified" { + const allocator = std.testing.allocator; + + const config = TargetsConfig{ + .files_dir = null, + .exe = &.{}, + .static_lib = &.{}, + .shared_lib = &.{}, + }; + + const result = try validateTargetFilesExist(allocator, config, std.fs.cwd()); + try std.testing.expectEqual(ValidationResult{ .valid = {} }, result); +} + +test "validatePlatformHasTargets detects missing targets section" { + const allocator = std.testing.allocator; + const base = @import("base"); + + // Platform without targets section + const source = + \\platform "" + \\ requires { main : {} } + \\ exposes [] + \\ packages {} + \\ provides { main_for_host: "main" } + \\ + ; + + const source_copy = try allocator.dupe(u8, source); + defer allocator.free(source_copy); + + var env = try base.CommonEnv.init(allocator, source_copy); + defer env.deinit(allocator); + + var ast = try parse.parse(&env, allocator); + defer ast.deinit(allocator); + + const result = validatePlatformHasTargets(ast, "test/platform/main.roc"); + + switch (result) { + .missing_targets_section => |info| { + try std.testing.expectEqualStrings("test/platform/main.roc", info.platform_path); + }, + else => { + std.debug.print("Expected missing_targets_section but got {}\n", .{result}); + return error.UnexpectedResult; + }, + } +} + +test "validatePlatformHasTargets accepts platform with targets section" { + const allocator = std.testing.allocator; + const base = @import("base"); + + // Platform with targets section + const source = + \\platform "" + \\ requires { main : {} } + \\ exposes [] + \\ packages {} + \\ provides { main_for_host: "main" } + \\ targets: { + \\ exe: { + \\ x64linux: [app], + \\ arm64linux: [app], + \\ } + \\ } + \\ + ; + + const source_copy = try allocator.dupe(u8, source); + defer allocator.free(source_copy); + + var env = try base.CommonEnv.init(allocator, source_copy); + defer env.deinit(allocator); + + var ast = try parse.parse(&env, allocator); + defer ast.deinit(allocator); + + const result = validatePlatformHasTargets(ast, "test/platform/main.roc"); + + try std.testing.expectEqual(ValidationResult{ .valid = {} }, result); +} + +test "validatePlatformHasTargets skips non-platform headers" { + const allocator = std.testing.allocator; + const base = @import("base"); + + // App module (not a platform) + const source = + \\app [main] { pf: platform "some-platform" } + \\ + \\main = {} + \\ + ; + + const source_copy = try allocator.dupe(u8, source); + defer allocator.free(source_copy); + + var env = try base.CommonEnv.init(allocator, source_copy); + defer env.deinit(allocator); + + var ast = try parse.parse(&env, allocator); + defer ast.deinit(allocator); + + const result = validatePlatformHasTargets(ast, "app/main.roc"); + + // Non-platform headers should return valid (they don't need targets) + try std.testing.expectEqual(ValidationResult{ .valid = {} }, result); +} + +test "validatePlatformHasTargets accepts platform with multiple target types" { + const allocator = std.testing.allocator; + const base = @import("base"); + + // Platform with exe and static_lib targets + const source = + \\platform "" + \\ requires { main : {} } + \\ exposes [] + \\ packages {} + \\ provides { main_for_host: "main" } + \\ targets: { + \\ files: "targets/", + \\ exe: { + \\ x64linux: ["host.o", app], + \\ arm64mac: [app], + \\ }, + \\ static_lib: { + \\ x64mac: ["libhost.a"], + \\ } + \\ } + \\ + ; + + const source_copy = try allocator.dupe(u8, source); + defer allocator.free(source_copy); + + var env = try base.CommonEnv.init(allocator, source_copy); + defer env.deinit(allocator); + + var ast = try parse.parse(&env, allocator); + defer ast.deinit(allocator); + + const result = validatePlatformHasTargets(ast, "test/platform/main.roc"); + + try std.testing.expectEqual(ValidationResult{ .valid = {} }, result); +} + +test "validatePlatformHasTargets accepts platform with win_gui target" { + const allocator = std.testing.allocator; + const base = @import("base"); + + // Platform with win_gui special identifier + const source = + \\platform "" + \\ requires { main : {} } + \\ exposes [] + \\ packages {} + \\ provides { main_for_host: "main" } + \\ targets: { + \\ exe: { + \\ x64win: [win_gui], + \\ } + \\ } + \\ + ; + + const source_copy = try allocator.dupe(u8, source); + defer allocator.free(source_copy); + + var env = try base.CommonEnv.init(allocator, source_copy); + defer env.deinit(allocator); + + var ast = try parse.parse(&env, allocator); + defer ast.deinit(allocator); + + const result = validatePlatformHasTargets(ast, "test/platform/main.roc"); + + try std.testing.expectEqual(ValidationResult{ .valid = {} }, result); +} + +test "TargetsConfig.fromAST extracts targets configuration" { + const allocator = std.testing.allocator; + const base = @import("base"); + + // Platform with various targets + const source = + \\platform "" + \\ requires { main : {} } + \\ exposes [] + \\ packages {} + \\ provides { main_for_host: "main" } + \\ targets: { + \\ files: "targets/", + \\ exe: { + \\ x64linux: ["host.o", app], + \\ arm64linux: [app], + \\ } + \\ } + \\ + ; + + const source_copy = try allocator.dupe(u8, source); + defer allocator.free(source_copy); + + var env = try base.CommonEnv.init(allocator, source_copy); + defer env.deinit(allocator); + + var ast = try parse.parse(&env, allocator); + defer ast.deinit(allocator); + + // Try to extract targets config from the AST + const maybe_config = try TargetsConfig.fromAST(allocator, ast); + try std.testing.expect(maybe_config != null); + + const config = maybe_config.?; + defer { + for (config.exe) |spec| { + allocator.free(spec.items); + } + allocator.free(config.exe); + } + + // Check files_dir + try std.testing.expect(config.files_dir != null); + try std.testing.expectEqualStrings("targets/", config.files_dir.?); + + // Check exe targets + try std.testing.expectEqual(@as(usize, 2), config.exe.len); +} diff --git a/src/cli/test/fx_platform_test.zig b/src/cli/test/fx_platform_test.zig new file mode 100644 index 0000000000..2ef80c979a --- /dev/null +++ b/src/cli/test/fx_platform_test.zig @@ -0,0 +1,1168 @@ +//! Integration tests for the fx platform with effectful functions. +//! +//! Tests that platform-provided hosted functions (like Stdout.line! and Stderr.line!) +//! can be properly invoked from Roc applications. +//! +//! NOTE: These tests depend on the roc binary being built via build.zig. The test step +//! has a dependency on roc_step, so the binary will be built automatically before tests run. +//! +//! IMPORTANT: Do NOT use --no-cache when running roc. The interpreted host doesn't change between +//! tests (we're testing app behaviour, not the platform), so using --no-cache would force unnecessary +//! re-linking on every test, making the test run much slower than is necessary. +//! +//! Test specs for IO-based tests are defined in fx_test_specs.zig and shared with +//! the cross-compilation test runner. + +const std = @import("std"); +const builtin = @import("builtin"); +const testing = std.testing; +const fx_test_specs = @import("fx_test_specs.zig"); + +// Wire up tests from fx_test_specs module +comptime { + std.testing.refAllDecls(fx_test_specs); +} + +const roc_binary_path = if (builtin.os.tag == .windows) ".\\zig-out\\bin\\roc.exe" else "./zig-out/bin/roc"; + +/// Options for running roc commands +const RunOptions = struct { + /// Additional command line arguments (e.g., "test", "check") + extra_args: []const []const u8 = &[_][]const u8{}, + /// Optional current working directory + cwd: ?[]const u8 = null, +}; + +/// Runs a roc command and returns the result. +fn runRoc(allocator: std.mem.Allocator, roc_file: []const u8, options: RunOptions) !std.process.Child.RunResult { + var args = std.ArrayList([]const u8){}; + defer args.deinit(allocator); + + try args.append(allocator, roc_binary_path); + try args.appendSlice(allocator, options.extra_args); + try args.append(allocator, roc_file); + + return try std.process.Child.run(.{ + .allocator = allocator, + .argv = args.items, + .cwd = options.cwd, + }); +} + +/// Helper to check if a run result indicates success (exit code 0) +fn checkSuccess(result: std.process.Child.RunResult) !void { + // Check for GPA (General Purpose Allocator) errors in stderr + // These indicate memory bugs like alignment mismatches, double frees, etc. + if (std.mem.indexOf(u8, result.stderr, "error(gpa):") != null) { + std.debug.print("Memory error detected (GPA)\n", .{}); + std.debug.print("STDOUT: {s}\n", .{result.stdout}); + std.debug.print("STDERR: {s}\n", .{result.stderr}); + return error.MemoryError; + } + + switch (result.term) { + .Exited => |code| { + if (code != 0) { + std.debug.print("Run failed with exit code {}\n", .{code}); + std.debug.print("STDOUT: {s}\n", .{result.stdout}); + std.debug.print("STDERR: {s}\n", .{result.stderr}); + return error.RunFailed; + } + }, + .Signal => |sig| { + std.debug.print("Process terminated by signal: {}\n", .{sig}); + std.debug.print("STDOUT: {s}\n", .{result.stdout}); + std.debug.print("STDERR: {s}\n", .{result.stderr}); + return error.SegFault; + }, + else => { + std.debug.print("Run terminated abnormally: {}\n", .{result.term}); + std.debug.print("STDOUT: {s}\n", .{result.stdout}); + std.debug.print("STDERR: {s}\n", .{result.stderr}); + return error.RunFailed; + }, + } +} + +/// Helper to check if a run result indicates failure (non-zero exit code) +/// This verifies the process exited cleanly with a non-zero code, NOT that it crashed. +fn checkFailure(result: std.process.Child.RunResult) !void { + switch (result.term) { + .Exited => |code| { + if (code == 0) { + std.debug.print("ERROR: roc succeeded but we expected it to fail\n", .{}); + return error.UnexpectedSuccess; + } + // Non-zero exit code is expected - this is a clean failure + }, + .Signal => |sig| { + // A crash is NOT the same as a clean failure - report it as an error + std.debug.print("ERROR: Process crashed with signal {} (expected clean failure with non-zero exit code)\n", .{sig}); + std.debug.print("STDOUT: {s}\n", .{result.stdout}); + std.debug.print("STDERR: {s}\n", .{result.stderr}); + return error.SegFault; + }, + else => { + std.debug.print("ERROR: Process terminated abnormally: {} (expected clean failure with non-zero exit code)\n", .{result.term}); + std.debug.print("STDOUT: {s}\n", .{result.stdout}); + std.debug.print("STDERR: {s}\n", .{result.stderr}); + return error.RunFailed; + }, + } +} + +/// Runs a roc app with --test mode using the given IO spec. +/// Spec format: "0stdout|2>stderr" (pipe-separated) +/// Returns success if the app's IO matches the spec exactly. +fn runRocTest(allocator: std.mem.Allocator, roc_file: []const u8, spec: []const u8) !std.process.Child.RunResult { + return try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary_path, + roc_file, + "--", + "--test", + spec, + }, + }); +} + +/// Helper to check if a test mode run succeeded (exit code 0, empty output) +fn checkTestSuccess(result: std.process.Child.RunResult) !void { + // Check for GPA (General Purpose Allocator) errors in stderr + // These indicate memory bugs like alignment mismatches, double frees, etc. + if (std.mem.indexOf(u8, result.stderr, "error(gpa):") != null) { + std.debug.print("Memory error detected (GPA)\n", .{}); + std.debug.print("STDERR: {s}\n", .{result.stderr}); + return error.MemoryError; + } + + switch (result.term) { + .Exited => |code| { + if (code != 0) { + std.debug.print("Test failed with exit code {}\n", .{code}); + std.debug.print("STDERR: {s}\n", .{result.stderr}); + return error.TestFailed; + } + }, + .Signal => |sig| { + std.debug.print("Process terminated by signal: {}\n", .{sig}); + std.debug.print("STDERR: {s}\n", .{result.stderr}); + return error.SegFault; + }, + else => { + std.debug.print("Test terminated abnormally: {}\n", .{result.term}); + std.debug.print("STDERR: {s}\n", .{result.stderr}); + return error.TestFailed; + }, + } +} + +// IO Spec Tests (using shared specs from fx_test_specs.zig) +// These tests use the --test mode with IO specifications to verify that +// roc applications produce the expected stdout/stderr output for given stdin. +// The specs are defined in fx_test_specs.zig and shared with the cross-compile +// test runner. + +test "fx platform IO spec tests" { + const allocator = testing.allocator; + + var passed: usize = 0; + var failed: usize = 0; + + for (fx_test_specs.io_spec_tests) |spec| { + const result = runRocTest(allocator, spec.roc_file, spec.io_spec) catch |err| { + std.debug.print("\n[FAIL] {s}: failed to run: {}\n", .{ spec.roc_file, err }); + failed += 1; + continue; + }; + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + + checkTestSuccess(result) catch |err| { + std.debug.print("\n[FAIL] {s}: {}\n", .{ spec.roc_file, err }); + if (spec.description.len > 0) { + std.debug.print(" Description: {s}\n", .{spec.description}); + } + failed += 1; + continue; + }; + + passed += 1; + } + + // Print summary + const total = passed + failed; + if (failed > 0) { + std.debug.print("\n{}/{} IO spec tests passed ({} failed)\n", .{ passed, total, failed }); + return error.SomeTestsFailed; + } +} + +test "fx platform expect with main" { + const allocator = testing.allocator; + + // Run `roc test` on the app that has both main! and an expect + // Note: `roc test` only evaluates expect statements, it does not run main! + const run_result = try runRoc(allocator, "test/fx/expect_with_main.roc", .{ .extra_args = &[_][]const u8{"test"} }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); + + // When all tests pass produce short output message + try testing.expectStringStartsWith(run_result.stdout, "All (1) tests passed in "); + try testing.expectEqualStrings("", run_result.stderr); +} + +test "fx platform expect with numeric literal" { + const allocator = testing.allocator; + + // Run `roc test` on an app that compares a typed variable with a numeric literal + // This tests that numeric literals in top-level expects are properly typed + const run_result = try runRoc(allocator, "test/fx/expect_with_literal.roc", .{ .extra_args = &[_][]const u8{"test"} }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); + + // When all tests pass produce short output message + try testing.expectStringStartsWith(run_result.stdout, "All (1) tests passed in "); + try testing.expectEqualStrings("", run_result.stderr); +} + +test "fx platform all_syntax_test.roc prints expected output" { + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/all_syntax_test.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); + + const expected_stdout = + "Hello, world!\n" ++ + "Hello, world! (using alias)\n" ++ + "{ diff: 5, div: 2, div_trunc: 2, eq: False, gt: True, gteq: True, lt: False, lteq: False, neg: -10, neq: True, prod: 50, rem: 0, sum: 15 }\n" ++ + "{ bool_and_keyword: False, bool_or_keyword: True, not_a: False }\n" ++ + "\"One Two\"\n" ++ + "\"Three Four\"\n" ++ + "The color is red.\n" ++ + "78\n" ++ + "Success\n" ++ + "Line 1\n" ++ + "Line 2\n" ++ + "Line 3\n" ++ + "Unicode escape sequence: \u{00A0}\n" ++ + "This is an effectful function!\n" ++ + "15\n" ++ + "42\n" ++ + "NotOneTwoNotFive\n" ++ + "(\"Roc\", 1)\n" ++ + "Builtin.List.[\"a\", \"b\"]\n" ++ + "(\"Roc\", 1, 1, \"Roc\")\n" ++ + "10\n" ++ + "{ age: 31, name: \"Alice\" }\n" ++ + "{ binary: 5, explicit_dec: 5, explicit_f32: 5, explicit_f64: 5, explicit_i128: 5, explicit_i16: 5, explicit_i32: 5, explicit_i64: 5, explicit_i8: 5, explicit_u128: 5, explicit_u16: 5, explicit_u32: 5, explicit_u64: 5, explicit_u8: 5, hex: 5, octal: 5, usage_based: 5 }\n" ++ + "False\n" ++ + "99\n" ++ + "\"12345.0\"\n"; + + try testing.expectEqualStrings(expected_stdout, run_result.stdout); + try testing.expectEqualStrings("ROC DBG: 42\n", run_result.stderr); +} + +test "fx platform match returning string" { + // Tests that match expressions with string returns work correctly + const allocator = testing.allocator; + + const result = try runRocTest(allocator, "test/fx/match_str_return.roc", "1>0"); + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + + try checkTestSuccess(result); +} + +test "fx platform match with wildcard" { + // Tests that wildcard patterns in match expressions work correctly + const allocator = testing.allocator; + + const result = try runRocTest(allocator, "test/fx/match_with_wildcard.roc", "1>0"); + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + + try checkTestSuccess(result); +} + +test "fx platform dbg missing return value" { + const allocator = testing.allocator; + + // Run an app that uses dbg as the last expression in main!. + // dbg is treated as a statement (side-effect only) when it's the final + // expression in a block, so the block returns {} as expected by main!. + const run_result = try runRoc(allocator, "test/fx/dbg_missing_return.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); + + // Verify that the dbg output was printed + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "this should work now") != null); +} + +test "fx platform check unused state var reports correct errors" { + const allocator = testing.allocator; + + // Run `roc check` on an app with unused variables and type annotations + // This test checks that the compiler reports the correct errors and doesn't + // produce extraneous unrelated errors from platform module resolution + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary_path, + "check", + "test/fx/unused_state_var.roc", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + // The check should fail with errors + switch (run_result.term) { + .Exited => |code| { + if (code == 0) { + std.debug.print("ERROR: roc check succeeded but we expected it to fail with errors\n", .{}); + return error.UnexpectedSuccess; + } + }, + else => { + std.debug.print("Run terminated abnormally: {}\n", .{run_result.term}); + return error.RunFailed; + }, + } + + const stderr = run_result.stderr; + + // Count occurrences of each error type + var unused_variable_count: usize = 0; + var module_not_found_count: usize = 0; + var exposed_but_not_defined_count: usize = 0; + + var line_iter = std.mem.splitScalar(u8, stderr, '\n'); + while (line_iter.next()) |line| { + if (std.mem.indexOf(u8, line, "UNUSED VARIABLE") != null) { + unused_variable_count += 1; + } else if (std.mem.indexOf(u8, line, "MODULE NOT FOUND") != null) { + module_not_found_count += 1; + } else if (std.mem.indexOf(u8, line, "EXPOSED BUT NOT DEFINED") != null) { + exposed_but_not_defined_count += 1; + } + } + + // We expect exactly 2 UNUSED VARIABLE errors + // We should NOT get MODULE NOT FOUND or EXPOSED BUT NOT DEFINED errors + // (those were the extraneous errors this test was created to catch) + // + // Note: There are other errors (TYPE MISMATCH, UNDEFINED VARIABLE for main!, + // COMPTIME CRASH) that are pre-existing bugs related to platform/app interaction + // and should be fixed separately. + var test_passed = true; + + if (unused_variable_count != 2) { + std.debug.print("\n❌ UNUSED VARIABLE count mismatch: expected 2, got {d}\n", .{unused_variable_count}); + test_passed = false; + } + + if (module_not_found_count != 0) { + std.debug.print("❌ MODULE NOT FOUND (extraneous): expected 0, got {d}\n", .{module_not_found_count}); + test_passed = false; + } + + if (exposed_but_not_defined_count != 0) { + std.debug.print("❌ EXPOSED BUT NOT DEFINED (extraneous): expected 0, got {d}\n", .{exposed_but_not_defined_count}); + test_passed = false; + } + + if (!test_passed) { + std.debug.print("\n========== FULL ROC CHECK OUTPUT ==========\n", .{}); + std.debug.print("STDERR:\n{s}\n", .{stderr}); + std.debug.print("==========================================\n\n", .{}); + return error.ExtraneousErrorsFound; + } +} + +test "fx platform checked directly finds sibling modules" { + // When checking a platform module directly (not through an app), sibling .roc + // files in the same directory should be discovered automatically. This means + // we should NOT get MODULE NOT FOUND errors for Stdout/Stderr/Stdin since + // those files exist in the same directory as main.roc. + const allocator = std.testing.allocator; + + // Check the platform module directly (not through an app) + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary_path, + "check", + "test/fx/platform/main.roc", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + const stderr = run_result.stderr; + + // Count MODULE NOT FOUND errors - we should get 0 since sibling modules are discovered + var module_not_found_count: usize = 0; + + var line_iter = std.mem.splitScalar(u8, stderr, '\n'); + while (line_iter.next()) |line| { + if (std.mem.indexOf(u8, line, "MODULE NOT FOUND") != null) { + module_not_found_count += 1; + } + } + + // When checking a platform directly, sibling modules should be discovered, + // so we should NOT get MODULE NOT FOUND errors for valid imports. + if (module_not_found_count != 0) { + std.debug.print("\n❌ Expected 0 MODULE NOT FOUND errors (siblings should be discovered), got {d}\n", .{module_not_found_count}); + std.debug.print("\n========== FULL ROC CHECK OUTPUT ==========\n", .{}); + std.debug.print("STDERR:\n{s}\n", .{stderr}); + std.debug.print("==========================================\n\n", .{}); + return error.UnexpectedModuleNotFoundErrors; + } +} + +test "fx platform string interpolation type mismatch" { + const allocator = testing.allocator; + + // Run an app that tries to interpolate a U8 (non-Str) type in a string. + // This should fail with a type error because string interpolation only accepts Str. + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary_path, + "test/fx/num_method_call.roc", + "--allow-errors", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + // The program should run (exit 0) with --allow-errors despite type errors + switch (run_result.term) { + .Exited => |code| { + try testing.expectEqual(@as(u8, 0), code); + }, + else => { + std.debug.print("Run terminated abnormally: {}\n", .{run_result.term}); + std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.RunFailed; + }, + } + + // Verify the error output contains proper diagnostic info + // Should show TYPE MISMATCH error with the type information + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "TYPE MISMATCH") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "U8") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Str") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "Found 1 error") != null); + + // The program should still produce output (it runs despite errors) + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "two:") != null); +} + +test "fx platform run from different cwd" { + // Regression test: Running roc from a different current working directory + // than the project root should still work. Previously this failed with + // "error.InvalidAppPath" because the path resolution didn't handle + // running from a subdirectory correctly. + const allocator = testing.allocator; + + // Get absolute path to roc binary since we'll change cwd + const roc_abs_path = try std.fs.cwd().realpathAlloc(allocator, roc_binary_path); + defer allocator.free(roc_abs_path); + + // Run roc from the test/fx directory with a relative path to app.roc + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_abs_path, + "app.roc", + }, + .cwd = "test/fx", + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + switch (run_result.term) { + .Exited => |code| { + if (code != 0) { + std.debug.print("Run failed with exit code {}\n", .{code}); + std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.RunFailed; + } + }, + else => { + std.debug.print("Run terminated abnormally: {}\n", .{run_result.term}); + std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.RunFailed; + }, + } + + // Verify stdout contains expected messages + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Hello from stdout!") != null); +} + +test "drop_prefix segfault regression" { + // Regression test: Calling drop_prefix on a string literal and assigning + // the result to an unused variable causes a segfault. + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/drop_prefix_segfault.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); +} + +test "drop_prefix match use-after-free regression" { + // Regression test: Calling drop_prefix on a string literal and using the + // result in a match expression causes a use-after-free panic. + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/drop_prefix_match_uaf.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); + + // Also check for panic messages in stderr that indicate use-after-free + if (std.mem.indexOf(u8, run_result.stderr, "panic") != null or + std.mem.indexOf(u8, run_result.stderr, "use-after-free") != null or + std.mem.indexOf(u8, run_result.stderr, "Invalid pointer") != null) + { + std.debug.print("Detected memory safety panic in stderr:\n{s}\n", .{run_result.stderr}); + return error.UseAfterFree; + } +} + +test "multiline string split_on" { + // Tests splitting a multiline string and iterating over the lines. + // This is a regression test to ensure split_on works correctly with + // multiline strings and doesn't cause memory issues. + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/multiline_split_leak.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); + + // Verify the output contains lines from the multiline string + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "This is a longer line number one") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "This is a longer line number two") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "L68") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "The last line is here") != null); +} + +test "big string equality regression" { + // Regression test: String literals of length >= 24 (big strings) must work + // correctly in expect expressions. This tests the single-segment string + // fast path in str_collect which previously caused use-after-free. + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/big_string_equality.roc", .{ .extra_args = &[_][]const u8{"test"} }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); + + // Check for panic messages in stderr that indicate use-after-free + if (std.mem.indexOf(u8, run_result.stderr, "panic") != null or + std.mem.indexOf(u8, run_result.stderr, "use-after-free") != null or + std.mem.indexOf(u8, run_result.stderr, "Use-after-free") != null) + { + std.debug.print("Detected memory safety panic in stderr:\n{s}\n", .{run_result.stderr}); + return error.UseAfterFree; + } +} + +test "fx platform expect with toplevel numeric" { + const allocator = testing.allocator; + + // Run the app + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary_path, + "test/fx/expect_with_toplevel_numeric.roc", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + switch (run_result.term) { + .Exited => |code| { + if (code != 0) { + std.debug.print("Run failed with exit code {}\n", .{code}); + std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.RunFailed; + } + }, + else => { + std.debug.print("Run terminated abnormally: {}\n", .{run_result.term}); + std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.RunFailed; + }, + } + + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "hello") != null); + + // Run `roc test` since this file has a top-level expect + const test_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary_path, + "test", + "test/fx/expect_with_toplevel_numeric.roc", + }, + }); + defer allocator.free(test_result.stdout); + defer allocator.free(test_result.stderr); + + switch (test_result.term) { + .Exited => |code| { + if (code != 0) { + std.debug.print("Test failed with exit code {}\n", .{code}); + std.debug.print("STDOUT: {s}\n", .{test_result.stdout}); + std.debug.print("STDERR: {s}\n", .{test_result.stderr}); + return error.TestFailed; + } + }, + else => { + std.debug.print("Test terminated abnormally: {}\n", .{test_result.term}); + std.debug.print("STDOUT: {s}\n", .{test_result.stdout}); + std.debug.print("STDERR: {s}\n", .{test_result.stderr}); + return error.TestFailed; + }, + } +} + +// TODO: Fix test7.roc - currently fails with "UNRECOGNIZED SYNTAX" for `_ = x` pattern +// test "fx platform test7" { +// const allocator = testing.allocator; +// +// const run_result = try std.process.Child.run(.{ +// .allocator = allocator, +// .argv = &[_][]const u8{ +// "roc_binary_path", +// "test/fx/test7.roc", +// }, +// }); +// defer allocator.free(run_result.stdout); +// defer allocator.free(run_result.stderr); + +// switch (run_result.term) { +// .Exited => |code| { +// if (code != 0) { +// std.debug.print("Run failed with exit code {}\n", .{code}); +// std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); +// std.debug.print("STDERR: {s}\n", .{run_result.stderr}); +// return error.RunFailed; +// } +// }, +// else => { +// std.debug.print("Run terminated abnormally: {}\n", .{run_result.term}); +// std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); +// std.debug.print("STDERR: {s}\n", .{run_result.stderr}); +// return error.RunFailed; +// }, +// } + +// try testing.expect(std.mem.indexOf(u8, run_result.stdout, "done") != null); +// } + +// TODO: Fix test8.roc - currently fails with "UNRECOGNIZED SYNTAX" for `_ = x` pattern +// test "fx platform test8" { +// const allocator = testing.allocator; +// +// const run_result = try std.process.Child.run(.{ +// .allocator = allocator, +// .argv = &[_][]const u8{ +// "roc_binary_path", +// "test/fx/test8.roc", +// }, +// }); +// defer allocator.free(run_result.stdout); +// defer allocator.free(run_result.stderr); + +// switch (run_result.term) { +// .Exited => |code| { +// if (code != 0) { +// std.debug.print("Run failed with exit code {}\n", .{code}); +// std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); +// std.debug.print("STDERR: {s}\n", .{run_result.stderr}); +// return error.RunFailed; +// } +// }, +// else => { +// std.debug.print("Run terminated abnormally: {}\n", .{run_result.term}); +// std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); +// std.debug.print("STDERR: {s}\n", .{run_result.stderr}); +// return error.RunFailed; +// }, +// } + +// try testing.expect(std.mem.indexOf(u8, run_result.stdout, "done") != null); +// } + +// TODO: Fix test9.roc - currently fails with "UNRECOGNIZED SYNTAX" for `_ = y` pattern +// test "fx platform test9" { +// const allocator = testing.allocator; +// +// const run_result = try std.process.Child.run(.{ +// .allocator = allocator, +// .argv = &[_][]const u8{ +// "roc_binary_path", +// "test/fx/test9.roc", +// }, +// }); +// defer allocator.free(run_result.stdout); +// defer allocator.free(run_result.stderr); + +// switch (run_result.term) { +// .Exited => |code| { +// if (code != 0) { +// std.debug.print("Run failed with exit code {}\n", .{code}); +// std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); +// std.debug.print("STDERR: {s}\n", .{run_result.stderr}); +// return error.RunFailed; +// } +// }, +// else => { +// std.debug.print("Run terminated abnormally: {}\n", .{run_result.term}); +// std.debug.print("STDOUT: {s}\n", .{run_result.stdout}); +// std.debug.print("STDERR: {s}\n", .{run_result.stderr}); +// return error.RunFailed; +// }, +// } + +// try testing.expect(std.mem.indexOf(u8, run_result.stdout, "done") != null); +// } + +test "fx platform test_type_mismatch" { + const allocator = testing.allocator; + + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary_path, + "test/fx/test_type_mismatch.roc", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + // This file is expected to fail compilation with a type mismatch error + // The to_inspect method returns I64 instead of Str + switch (run_result.term) { + .Exited => |code| { + if (code != 0) { + // Expected to fail - check for type mismatch error message + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "TYPE MISMATCH") != null); + } else { + std.debug.print("Expected compilation error but succeeded\n", .{}); + return error.UnexpectedSuccess; + } + }, + else => { + // Abnormal termination should also indicate error + std.debug.print("Run terminated abnormally: {}\n", .{run_result.term}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "TYPE MISMATCH") != null); + }, + } +} + +test "fx platform issue8433" { + const allocator = testing.allocator; + + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary_path, + "test/fx/issue8433.roc", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + // This file is expected to fail compilation with a MISSING METHOD error + switch (run_result.term) { + .Exited => |code| { + if (code != 0) { + // Expected to fail - check for missing method error message + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "MISSING METHOD") != null); + } else { + std.debug.print("Expected compilation error but succeeded\n", .{}); + return error.UnexpectedSuccess; + } + }, + else => { + // Abnormal termination should also indicate error + std.debug.print("Run terminated abnormally: {}\n", .{run_result.term}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "MISSING METHOD") != null); + }, + } +} + +test "run aborts on type errors by default" { + // Tests that roc run aborts when there are type errors (without --allow-errors) + const allocator = testing.allocator; + + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary_path, + "test/fx/run_allow_errors.roc", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + // Should fail with type errors + try checkFailure(run_result); + + // Should show the errors + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "UNDEFINED VARIABLE") != null); +} + +test "run aborts on parse errors by default" { + // Tests that roc run aborts when there are parse errors (without --allow-errors) + const allocator = testing.allocator; + + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + "./zig-out/bin/roc", + "test/fx/parse_error.roc", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + // Should fail with type errors + try checkFailure(run_result); + + // Should show the errors + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "PARSE ERROR") != null); +} + +test "run with --allow-errors attempts execution despite type errors" { + // Tests that roc run --allow-errors attempts to execute even with type errors + const allocator = testing.allocator; + + const run_result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary_path, + "test/fx/run_allow_errors.roc", + "--allow-errors", + }, + }); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + // Should still show the errors + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "UNDEFINED VARIABLE") != null); + + // The program will attempt to run and likely crash, which is expected behavior + // We just verify it didn't abort during type checking +} + +test "run allows warnings without blocking execution" { + // Tests that warnings don't block execution (they never should) + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/run_warning_only.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); + + // Should show the warning + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "UNUSED VARIABLE") != null); + + // Should produce output (runs successfully) + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Hello, World!") != null); +} + +test "fx platform method inspect on string" { + // Tests that Str.inspect works correctly on a string value + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/test_method_inspect.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + // Str.inspect now exists - this should succeed and output the inspected string + try checkSuccess(run_result); + + // Should output the inspected string value + try testing.expectEqualStrings("\"hello\"\n", run_result.stdout); +} + +test "fx platform if-expression closure capture regression" { + // Regression test: Variables bound inside an if-expression's block were + // incorrectly being captured as free variables by the enclosing lambda, + // causing a crash with "e_closure: failed to resolve capture value". + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/if-closure-capture.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); +} + +test "fx platform var with string interpolation segfault" { + // Regression test: Using `var` variables with string interpolation causes segfault. + // The code calls fnA! multiple times, each using var state variables, and + // interpolates the results into strings. + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/var_interp_segfault.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); + + // Verify the expected output + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "A1: 1") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "A2: 1") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "A3: 1") != null); +} + +test "fx platform sublist method on inferred type" { + // Regression test: Calling .sublist() method on a List(U8) from "".to_utf8() + // causes a segfault when the variable doesn't have an explicit type annotation. + // Error was: "Roc crashed: Error evaluating from shared memory: InvalidMethodReceiver" + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/sublist_method_segfault.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); +} + +test "fx platform repeating pattern segfault" { + // Regression test: This test exposed a compiler bug where variables used multiple times + // in consuming positions didn't get proper refcount handling. Specifically, + // in `repeat_helper(acc.concat(list), list, n-1)`, the variable `list` is + // passed to both concat (consuming) and to the recursive call (consuming). + // The compiler must insert a copy/incref for the second use to avoid use-after-free. + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/repeating_pattern_segfault.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); +} + +test "fx platform runtime stack overflow" { + // Tests that stack overflow in a running Roc program is caught and reported + // with a helpful error message instead of crashing with a raw signal. + // + // The Roc program contains an infinitely recursive function that will + // overflow the stack at runtime. Once proper stack overflow handling is + // implemented in the host/platform, this test will pass. + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/stack_overflow_runtime.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + // Stack overflow can be caught by either: + // 1. The Roc interpreter (exit code 1, "overflowed its stack memory" message) - most common + // 2. The SIGABRT signal handler (exit code 134) - if native stack overflow handling is used + switch (run_result.term) { + .Exited => |code| { + if (code == 134) { + // Stack overflow was caught by native signal handler + // Verify the helpful error message was printed + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "overflowed its stack memory") != null); + } else if (code == 1) { + // Stack overflow was caught by the interpreter - this is the expected case + // The interpreter detects excessive work stack depth and reports the error + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "overflowed its stack memory") != null); + } else if (code == 139) { + // Exit code 139 = 128 + 11 (SIGSEGV) - stack overflow was NOT handled + // The Roc program crashed with a segfault that wasn't caught + std.debug.print("\n", .{}); + std.debug.print("Stack overflow handling NOT YET IMPLEMENTED for Roc programs.\n", .{}); + std.debug.print("Process crashed with SIGSEGV (exit code 139).\n", .{}); + std.debug.print("Expected: exit code 1 or 134 with stack overflow message\n", .{}); + return error.StackOverflowNotHandled; + } else { + std.debug.print("Unexpected exit code: {}\n", .{code}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.UnexpectedExitCode; + } + }, + .Signal => |sig| { + // Process was killed directly by a signal (likely SIGSEGV = 11). + std.debug.print("\n", .{}); + std.debug.print("Stack overflow handling NOT YET IMPLEMENTED for Roc programs.\n", .{}); + std.debug.print("Process was killed by signal: {}\n", .{sig}); + std.debug.print("Expected: exit code 1 or 134 with stack overflow message\n", .{}); + return error.StackOverflowNotHandled; + }, + else => { + std.debug.print("Unexpected termination: {}\n", .{run_result.term}); + return error.UnexpectedTermination; + }, + } +} + +test "fx platform runtime division by zero" { + // Tests that division by zero in a running Roc program is caught and reported + // with a helpful error message instead of crashing with a raw signal. + // + // The error can be caught by either: + // 1. The Roc interpreter (exit code 1, "DivisionByZero" message) - most common + // 2. The SIGFPE signal handler (exit code 136, "divided by zero" message) - native code + const allocator = testing.allocator; + + // The Roc program uses a var to prevent compile-time constant folding + const run_result = try runRoc(allocator, "test/fx/division_by_zero.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + switch (run_result.term) { + .Exited => |code| { + if (code == 136) { + // Division by zero was caught by the SIGFPE handler (native code) + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "divided by zero") != null); + } else if (code == 1) { + // Division by zero was caught by the interpreter - this is the expected case + // The interpreter catches it and reports "DivisionByZero" + try testing.expect(std.mem.indexOf(u8, run_result.stderr, "DivisionByZero") != null); + } else { + std.debug.print("Unexpected exit code: {}\n", .{code}); + std.debug.print("STDERR: {s}\n", .{run_result.stderr}); + return error.UnexpectedExitCode; + } + }, + .Signal => |sig| { + // Process was killed directly by a signal without being caught + std.debug.print("\n", .{}); + std.debug.print("Division by zero was not caught!\n", .{}); + std.debug.print("Process was killed by signal: {}\n", .{sig}); + return error.DivisionByZeroNotHandled; + }, + else => { + std.debug.print("Unexpected termination: {}\n", .{run_result.term}); + return error.UnexpectedTermination; + }, + } +} + +test "fx platform inline expect fails as expected" { + // Regression test: inline expect inside main! should fail via the + // normal crash handler (Roc crashed: ...) instead of overflowing + // the stack and triggering the stack overflow handler. + const allocator = testing.allocator; + const run_result = try runRoc(allocator, "test/fx/issue8517.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + // Expect a clean failure (non-zero exit code, no signal) + try checkFailure(run_result); + + const stderr = run_result.stderr; + + // Should report a crash with the expect expression snippet + try testing.expect(std.mem.indexOf(u8, stderr, "1 == 2") != null); +} + +test "fx platform inline expect succeeds as expected" { + const allocator = testing.allocator; + + const result = try runRocTest(allocator, "test/fx/inline_expect_pass.roc", "1>All good."); + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + + try checkTestSuccess(result); +} + +test "fx platform index out of bounds in instantiate regression" { + // Regression test: A specific combination of features causes an index out of bounds + // panic in the type instantiation code (instantiate.zig:344). The panic occurs during + // type checking when instantiating a tag union type. + // + // The crash requires: + // - A value alias (day_input = demo_input) + // - A print! function using split_on().for_each!() + // - Two similar effectful functions (part1!, part2!) with: + // - for loop over input.trim().split_on() + // - print! call inside the for loop + // - parse_range call with ? operator + // - while loop calling a function with sublist() + // - has_repeating_pattern using slice->repeat(n // $d) with mutable var $d + // - String interpolation calling part2! + // + // The bug manifests as: panic: index out of bounds: index 2863311530, len 1035 + // The index 0xAAAAAAAA suggests uninitialized/corrupted memory. + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/index_oob_instantiate.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + // The compiler should not panic/crash. Once the bug is fixed, this test will pass. + // Currently it fails with a panic in instantiate.zig. + try checkSuccess(run_result); +} + +test "fx platform fold_rev static dispatch regression" { + // Regression test: Calling fold_rev with static dispatch (method syntax) panics, + // but calling it qualified as List.fold_rev(...) works fine. + // + // The panic occurs with: [1].fold_rev([], |elem, acc| acc.append(elem)) + // But this works: List.fold_rev([1], [], |elem, acc| acc.append(elem)) + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/fold_rev_static_dispatch.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); + + // Verify the expected output + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Start reverse") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Reversed: 3 elements") != null); + try testing.expect(std.mem.indexOf(u8, run_result.stdout, "Done") != null); +} + +test "external platform memory alignment regression" { + // This test verifies that external platforms with the memory alignment fix work correctly. + // The bug was in roc-platform-template-zig < 0.6 where rocDeallocFn used + // `roc_dealloc.alignment` directly instead of `@max(roc_dealloc.alignment, @alignOf(usize))`. + // Fixed in https://github.com/lukewilliamboswell/roc-platform-template-zig/releases/tag/0.6 + const allocator = testing.allocator; + + const run_result = try runRoc(allocator, "test/fx/aoc_day2.roc", .{}); + defer allocator.free(run_result.stdout); + defer allocator.free(run_result.stderr); + + try checkSuccess(run_result); +} diff --git a/src/cli/test/fx_test_specs.zig b/src/cli/test/fx_test_specs.zig new file mode 100644 index 0000000000..60a567b485 --- /dev/null +++ b/src/cli/test/fx_test_specs.zig @@ -0,0 +1,286 @@ +//! Shared test specifications for fx platform tests. +//! +//! This module defines IO specs for all fx platform tests that can be run +//! using the --test mode. These specs are shared between: +//! - Native Zig tests (fx_platform_test.zig) +//! - Unified test platform runner (test_runner.zig) +//! +//! IO Spec Format: "0stdout|2>stderr" (pipe-separated) +//! - 0text: expected stdout output +//! - 2>text: expected stderr output + +/// Test specification with a roc file path and expected IO spec +pub const TestSpec = struct { + /// Path to the roc file (relative to project root) + roc_file: []const u8, + /// IO spec for --test mode + io_spec: []const u8, + /// Optional description of what the test verifies + description: []const u8 = "", +}; + +/// All fx platform tests that can be run with --test mode IO specs. +/// These tests work with cross-compilation because they only test +/// the compiled binary's IO behavior, not build-time features. +pub const io_spec_tests = [_]TestSpec{ + // Basic effectful function tests + .{ + .roc_file = "test/fx/app.roc", + .io_spec = "1>Hello from stdout!|1>Line 1 to stdout|2>Line 2 to stderr|1>Line 3 to stdout|2>Error from stderr!", + .description = "Basic effectful functions: Stdout.line!, Stderr.line!", + }, + .{ + .roc_file = "test/fx/subdir/app.roc", + .io_spec = "1>Hello from stdout!|1>Line 1 to stdout|2>Line 2 to stderr|1>Line 3 to stdout|2>Error from stderr!", + .description = "Relative paths starting with ..", + }, + + // Stdin tests + .{ + .roc_file = "test/fx/stdin_to_stdout.roc", + .io_spec = "0test input", + .description = "Stdin to stdout passthrough", + }, + .{ + .roc_file = "test/fx/stdin_echo.roc", + .io_spec = "0hello world", + .description = "Stdin echo", + }, + .{ + .roc_file = "test/fx/stdin_test.roc", + .io_spec = "1>Before stdin|0After stdin", + .description = "Stdin with output before and after", + }, + .{ + .roc_file = "test/fx/stdin_simple.roc", + .io_spec = "0simple test", + .description = "Stdin to stderr", + }, + + // Match expression tests + .{ + .roc_file = "test/fx/match_str_return.roc", + .io_spec = "1>0", + .description = "Match expressions with string returns", + }, + .{ + .roc_file = "test/fx/match_with_wildcard.roc", + .io_spec = "1>0", + .description = "Wildcard patterns in match expressions", + }, + + // Opaque type tests + .{ + .roc_file = "test/fx/opaque_with_method.roc", + .io_spec = "1>My favourite color is Red", + .description = "Opaque type with attached method", + }, + + // Language feature tests + .{ + .roc_file = "test/fx/question_mark_operator.roc", + .io_spec = "1>hello", + .description = "Question mark operator for error propagation", + }, + .{ + .roc_file = "test/fx/numeric_fold.roc", + .io_spec = "1>Sum: 15.0", + .description = "List.fold with numeric accumulators", + }, + .{ + .roc_file = "test/fx/list_for_each.roc", + .io_spec = "1>Item: apple|1>Item: banana|1>Item: cherry", + .description = "List.for_each! with effectful callback", + }, + .{ + .roc_file = "test/fx/string_pattern_matching.roc", + .io_spec = "1>Hello Alice!|1>Hey Bob!", + .description = "Pattern matching on string literals", + }, + + // Basic output tests + .{ + .roc_file = "test/fx/hello_world.roc", + .io_spec = "1>Hello, world!", + .description = "Hello world", + }, + .{ + .roc_file = "test/fx/function_wrapper_stdout.roc", + .io_spec = "1>Hello from stdout!", + .description = "Function wrapper stdout", + }, + .{ + .roc_file = "test/fx/function_wrapper_multiline.roc", + .io_spec = "1>Hello from stdout!|1>Line 2", + .description = "Function wrapper multiline output", + }, + .{ + .roc_file = "test/fx/multiline_stdout.roc", + .io_spec = "1>Hello|1>World", + .description = "Multiple stdout lines", + }, + + // List and string tests + .{ + .roc_file = "test/fx/empty_list_get.roc", + .io_spec = "1>is err", + .description = "Empty list get returns error", + }, + .{ + .roc_file = "test/fx/str_interp_valid.roc", + .io_spec = "1>Hello, World!", + .description = "String interpolation", + }, + + // Lookup tests + .{ + .roc_file = "test/fx/numeric_lookup_test.roc", + .io_spec = "1>done", + .description = "Numeric lookup", + }, + .{ + .roc_file = "test/fx/string_lookup_test.roc", + .io_spec = "1>hello", + .description = "String lookup", + }, + .{ + .roc_file = "test/fx/test_direct_string.roc", + .io_spec = "1>Hello", + .description = "Direct string output", + }, + .{ + .roc_file = "test/fx/test_one_call.roc", + .io_spec = "1>Hello", + .description = "Single function call", + }, + .{ + .roc_file = "test/fx/test_with_wrapper.roc", + .io_spec = "1>Hello", + .description = "Function with wrapper", + }, + + // Inspect tests + .{ + .roc_file = "test/fx/inspect_compare_test.roc", + .io_spec = "1>With to_inspect: Custom::Red|1>Without to_inspect: ColorWithoutInspect.Red|1>Primitive: 42", + .description = "Inspect comparison with and without to_inspect", + }, + .{ + .roc_file = "test/fx/inspect_custom_test.roc", + .io_spec = "1>Color::Red|1>Expected: Color::Red", + .description = "Custom inspect implementation", + }, + .{ + .roc_file = "test/fx/inspect_nested_test.roc", + // Note: field order may differ from expected - record fields are rendered in their internal order + .io_spec = "1>{ color: Color::Red, count: 42, name: \"test\" }|1>Expected: { color: Color::Red, count: 42, name: \"test\" }", + .description = "Nested struct inspection", + }, + .{ + .roc_file = "test/fx/inspect_no_method_test.roc", + .io_spec = "1>Result: Color.Red|1>(Default rendering)", + .description = "Inspect without to_inspect method", + }, + .{ + .roc_file = "test/fx/inspect_record_test.roc", + .io_spec = "1>{ count: 42, name: \"test\" }", + .description = "Record inspection", + }, + .{ + .roc_file = "test/fx/inspect_wrong_sig_test.roc", + .io_spec = "1>Result: 1", + .description = "Inspect with wrong signature", + }, + .{ + .roc_file = "test/fx/inspect_open_tag_test.roc", + .io_spec = "1>Closed: TagB|1>With payload: Value(42)|1>Number: 123", + .description = "Str.inspect on tag unions", + }, + // Bug regression tests + .{ + .roc_file = "test/fx/unify_scratch_fresh_vars_rank_bug.roc", + .io_spec = "1>ok", + .description = "Regression test: unify scratch fresh_vars must be cleared between calls", + }, + .{ + .roc_file = "test/fx/recursive_tuple_list.roc", + .io_spec = "1>Result count: 4", + .description = "Regression test: recursive function with List of tuples and append", + }, + .{ + .roc_file = "test/fx/list_map_fallible.roc", + .io_spec = "1>done", + .description = "Regression test: List.map with fallible function (U64.from_str)", + }, + .{ + .roc_file = "test/fx/list_append_stdin_uaf.roc", + .io_spec = "0<000000010000000100000001|1>000000010000000100000001", + .description = "Regression test: List.append with effectful call on big string (24+ chars)", + }, + .{ + .roc_file = "test/fx/list_first_method.roc", + .io_spec = "1>ok", + .description = "Regression test: List.first with method syntax", + }, + .{ + .roc_file = "test/fx/list_first_function.roc", + .io_spec = "1>ok", + .description = "Regression test: List.first with function syntax", + }, + .{ + .roc_file = "test/fx/stdin_while_uaf.roc", + .io_spec = "0<123456789012345678901234|1>123456789012345678901234|0<|1>", + .description = "Regression test: Stdin.line! in while loop with 24 char input (heap-allocated string)", + }, + .{ + .roc_file = "test/fx/stdin_while_uaf.roc", + .io_spec = "0short|0<|1>", + .description = "Regression test: Stdin.line! in while loop with short input (small string optimization)", + }, + .{ + .roc_file = "test/fx/list_method_get.roc", + .io_spec = "1>is ok", + .description = "Regression test: List.get with method syntax (issue #8662)", + }, + .{ + .roc_file = "test/fx/issue8654.roc", + .io_spec = "1>False", + .description = "Regression test: Method lookup for nominal types in roc build executables (issue #8654)", + }, +}; + +/// Get the total number of IO spec tests +pub fn getTestCount() usize { + return io_spec_tests.len; +} + +/// Find a test spec by roc file path +pub fn findByPath(roc_file: []const u8) ?TestSpec { + for (io_spec_tests) |spec| { + if (std.mem.eql(u8, spec.roc_file, roc_file)) { + return spec; + } + } + return null; +} + +const std = @import("std"); + +test "all test specs have valid paths" { + for (io_spec_tests) |spec| { + // Just verify the paths are non-empty and start with test/fx + try std.testing.expect(spec.roc_file.len > 0); + try std.testing.expect(std.mem.startsWith(u8, spec.roc_file, "test/fx")); + try std.testing.expect(spec.io_spec.len > 0); + } +} + +test "find by path works" { + const found = findByPath("test/fx/hello_world.roc"); + try std.testing.expect(found != null); + try std.testing.expectEqualStrings("1>Hello, world!", found.?.io_spec); + + const not_found = findByPath("nonexistent.roc"); + try std.testing.expect(not_found == null); +} diff --git a/src/cli/test/platform_config.zig b/src/cli/test/platform_config.zig new file mode 100644 index 0000000000..733ae40c6e --- /dev/null +++ b/src/cli/test/platform_config.zig @@ -0,0 +1,263 @@ +//! Platform configurations for test platforms. +//! +//! This module defines configurations for all test platforms, including: +//! - Available targets +//! - Test app discovery +//! - Platform capabilities (native exec, IO specs, valgrind) + +const std = @import("std"); +const fx_test_specs = @import("fx_test_specs.zig"); + +/// Target information +pub const TargetInfo = struct { + name: []const u8, + requires_linux: bool, +}; + +/// Simple test file specification (no IO expectations) +pub const SimpleTestSpec = struct { + /// Path to the roc file (relative to project root) + roc_file: []const u8, + /// Description of what the test verifies + description: []const u8 = "", +}; + +/// How test apps are discovered for a platform +pub const TestApps = union(enum) { + /// Single app file (like int) + single: []const u8, + /// List of test specs with IO expectations (like fx) + spec_list: []const fx_test_specs.TestSpec, + /// List of simple test files without IO specs (like str) + simple_list: []const SimpleTestSpec, +}; + +/// Platform configuration +pub const PlatformConfig = struct { + name: []const u8, + base_dir: []const u8, + targets: []const TargetInfo, + test_apps: TestApps, + supports_native_exec: bool, + supports_io_specs: bool, + valgrind_safe: bool, +}; + +/// All available cross-compilation targets (superset) +pub const all_cross_targets = [_][]const u8{ + "x64musl", + "arm64musl", + "x64glibc", + "arm64glibc", +}; + +/// Standard targets for platforms with glibc support +const targets_with_glibc = [_]TargetInfo{ + .{ .name = "x64musl", .requires_linux = false }, + .{ .name = "arm64musl", .requires_linux = false }, + .{ .name = "x64glibc", .requires_linux = true }, + .{ .name = "arm64glibc", .requires_linux = true }, +}; + +/// Standard targets for platforms without glibc support +const targets_musl_only = [_]TargetInfo{ + .{ .name = "x64musl", .requires_linux = false }, + .{ .name = "arm64musl", .requires_linux = false }, +}; + +/// Targets for fx platforms (musl + Windows) +const targets_fx = [_]TargetInfo{ + .{ .name = "x64musl", .requires_linux = false }, + .{ .name = "arm64musl", .requires_linux = false }, + .{ .name = "x64win", .requires_linux = false }, + .{ .name = "arm64win", .requires_linux = false }, +}; + +/// Str platform test apps - test cross-module function calls +const str_tests = [_]SimpleTestSpec{ + // Basic test - no module imports from app + .{ + .roc_file = "test/str/app.roc", + .description = "Basic app with no platform module imports", + }, + + // Direct calls from app to each exposed module + .{ + .roc_file = "test/str/app_direct_utils.roc", + .description = "Direct call to Utils (base module, no imports)", + }, + .{ + .roc_file = "test/str/app_direct_core.roc", + .description = "Direct call to Core.wrap (Core imports Utils)", + }, + .{ + .roc_file = "test/str/app_direct_helper.roc", + .description = "Direct call to Helper.simple (no internal module calls)", + }, + .{ + .roc_file = "test/str/app_direct_helper2.roc", + .description = "Direct import of Helper (first in exposes list)", + }, + + // Transitive calls through modules + .{ + .roc_file = "test/str/app_transitive.roc", + .description = "Transitive call: app->Helper.wrap_fancy->Core.wrap", + }, + .{ + .roc_file = "test/str/app_core_tagged.roc", + .description = "Transitive call: app->Core.wrap_tagged->Utils.tag", + }, + + // Diamond dependency pattern + .{ + .roc_file = "test/str/app_diamond.roc", + .description = "Diamond: app->Helper->{Core->Utils, Utils}", + }, +}; + +/// All platform configurations +pub const platforms = [_]PlatformConfig{ + // INT PLATFORM - Integer operations + .{ + .name = "int", + .base_dir = "test/int", + .targets = &targets_with_glibc, + .test_apps = .{ .single = "app.roc" }, + .supports_native_exec = true, + .supports_io_specs = false, + .valgrind_safe = true, + }, + + // STR PLATFORM - String processing with multi-module tests + .{ + .name = "str", + .base_dir = "test/str", + .targets = &targets_with_glibc, + .test_apps = .{ .simple_list = &str_tests }, + .supports_native_exec = true, + .supports_io_specs = false, + .valgrind_safe = true, + }, + + // FX PLATFORM - Effectful (stdout, stderr, stdin) + .{ + .name = "fx", + .base_dir = "test/fx", + .targets = &targets_fx, + .test_apps = .{ .spec_list = &fx_test_specs.io_spec_tests }, + .supports_native_exec = true, + .supports_io_specs = true, + .valgrind_safe = false, // Has stdin tests + }, + + // FX-OPEN PLATFORM - Effectful with open union errors + .{ + .name = "fx-open", + .base_dir = "test/fx-open", + .targets = &targets_fx, + .test_apps = .{ .single = "app.roc" }, + .supports_native_exec = true, + .supports_io_specs = false, + .valgrind_safe = true, + }, +}; + +/// Find a platform configuration by name +pub fn findPlatform(name: []const u8) ?PlatformConfig { + for (platforms) |platform| { + if (std.mem.eql(u8, platform.name, name)) { + return platform; + } + } + return null; +} + +/// Find a target in a platform's target list +pub fn findTarget(platform: PlatformConfig, target_name: []const u8) ?TargetInfo { + for (platform.targets) |target| { + if (std.mem.eql(u8, target.name, target_name)) { + return target; + } + } + return null; +} + +/// Get list of all platform names +pub fn getPlatformNames() []const []const u8 { + comptime { + var names: [platforms.len][]const u8 = undefined; + for (platforms, 0..) |platform, i| { + names[i] = platform.name; + } + return &names; + } +} + +/// Get test app paths for a platform +pub fn getTestApps(platform: PlatformConfig) []const []const u8 { + switch (platform.test_apps) { + .single => |app| { + const result = [_][]const u8{app}; + return &result; + }, + .spec_list => |specs| { + // Return just the roc_file paths + var paths: [specs.len][]const u8 = undefined; + for (specs, 0..) |spec, i| { + paths[i] = spec.roc_file; + } + return &paths; + }, + .simple_list => |specs| { + // Return just the roc_file paths + var paths: [specs.len][]const u8 = undefined; + for (specs, 0..) |spec, i| { + paths[i] = spec.roc_file; + } + return &paths; + }, + } +} + +test "findPlatform works" { + const int_platform = findPlatform("int"); + try std.testing.expect(int_platform != null); + try std.testing.expectEqualStrings("test/int", int_platform.?.base_dir); + + const fx_platform = findPlatform("fx"); + try std.testing.expect(fx_platform != null); + try std.testing.expect(fx_platform.?.supports_io_specs); + + const unknown = findPlatform("nonexistent"); + try std.testing.expect(unknown == null); +} + +test "findTarget works" { + const int_platform = findPlatform("int").?; + + const musl = findTarget(int_platform, "x64musl"); + try std.testing.expect(musl != null); + try std.testing.expect(!musl.?.requires_linux); + + const glibc = findTarget(int_platform, "x64glibc"); + try std.testing.expect(glibc != null); + try std.testing.expect(glibc.?.requires_linux); + + const nonexistent = findTarget(int_platform, "x64windows"); + try std.testing.expect(nonexistent == null); +} + +test "fx platform has io specs" { + const fx_platform = findPlatform("fx").?; + try std.testing.expect(fx_platform.supports_io_specs); + + switch (fx_platform.test_apps) { + .spec_list => |specs| { + try std.testing.expect(specs.len > 0); + }, + .single => { + try std.testing.expect(false); // fx should have spec_list + }, + } +} diff --git a/src/cli/test/roc_subcommands.zig b/src/cli/test/roc_subcommands.zig new file mode 100644 index 0000000000..8608492648 --- /dev/null +++ b/src/cli/test/roc_subcommands.zig @@ -0,0 +1,614 @@ +//! End-to-end integration tests for roc subcommands using the actual roc CLI binary. + +const std = @import("std"); +const util = @import("util.zig"); + +test "roc check writes parse errors to stderr" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRoc(gpa, &.{ "check", "--no-cache" }, "test/cli/has_parse_error.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command failed (non-zero exit code) + try testing.expect(result.term != .Exited or result.term.Exited != 0); + + // 2. Stderr contains error information (THIS IS THE KEY TEST - without flush, this will be empty) + try testing.expect(result.stderr.len > 0); + + // 3. Stderr contains error reporting + const has_error = std.mem.indexOf(u8, result.stderr, "Failed to check") != null or + std.mem.indexOf(u8, result.stderr, "error") != null or + std.mem.indexOf(u8, result.stderr, "Unsupported") != null; + try testing.expect(has_error); +} + +test "roc check displays correct file path in parse error messages" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRoc(gpa, &.{ "check", "--no-cache" }, "test/cli/has_parse_error.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command failed (non-zero exit code) due to parse error + try testing.expect(result.term != .Exited or result.term.Exited != 0); + + // 2. Stderr contains error information + try testing.expect(result.stderr.len > 0); + + // 3. Stderr contains the actual file path, not mangled bytes + // The error message should include "has_parse_error.roc" in the location indicator + const has_file_path = std.mem.indexOf(u8, result.stderr, "has_parse_error.roc") != null; + try testing.expect(has_file_path); + + // 4. Stderr should NOT contain sequences of 0xaa bytes (indicates path encoding issue) + // When paths are mangled, they appear as repeated 0xaa bytes in the output + const mangled_path_pattern = [_]u8{ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa }; + const has_mangled_path = std.mem.indexOf(u8, result.stderr, &mangled_path_pattern) != null; + try testing.expect(!has_mangled_path); +} + +test "roc check succeeds on valid file" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRoc(gpa, &.{ "check", "--no-cache" }, "test/cli/simple_success.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command succeeded (zero exit code) + try testing.expect(result.term == .Exited and result.term.Exited == 0); + + // 2. Stderr should be empty or minimal for success + // (No errors should be reported) + const has_error = std.mem.indexOf(u8, result.stderr, "Failed to check") != null or + std.mem.indexOf(u8, result.stderr, "error") != null; + try testing.expect(!has_error); +} + +test "roc version outputs at least 5 chars to stdout" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRocCommand(gpa, &.{"version"}); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command succeeded (zero exit code) + try testing.expect(result.term == .Exited and result.term.Exited == 0); + + // 2. Stdout contains at least 5 characters + try testing.expect(result.stdout.len >= 5); +} + +// Once repl is implemented, this test should be updated to check for the expected output. +test "roc repl outputs at least 5 chars to stderr" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRocCommand(gpa, &.{"repl"}); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Output (stderr) contains at least 5 characters + try testing.expect(result.stderr.len >= 5); +} + +test "roc help contains Usage:" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRocCommand(gpa, &.{"help"}); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command succeeded (zero exit code) + try testing.expect(result.term == .Exited and result.term.Exited == 0); + + // 2. Stdout contains "Usage:" + const has_usage = std.mem.indexOf(u8, result.stdout, "Usage:") != null; + try testing.expect(has_usage); +} + +test "roc licenses contains =====" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRocCommand(gpa, &.{"licenses"}); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command succeeded (zero exit code) + try testing.expect(result.term == .Exited and result.term.Exited == 0); + + // 2. Stdout contains "=====" + const has_usage = std.mem.indexOf(u8, result.stdout, "=====") != null; + try testing.expect(has_usage); +} + +test "roc fmt --check fails on unformatted file" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRoc(gpa, &.{ "fmt", "--check" }, "test/cli/needs_formatting.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command failed (non-zero exit code) because file needs formatting + try testing.expect(result.term != .Exited or result.term.Exited != 0); + + // 2. Stderr or stdout contains formatting-related message + const has_format_msg = std.mem.indexOf(u8, result.stderr, "needs_formatting.roc") != null or + std.mem.indexOf(u8, result.stdout, "needs_formatting.roc") != null or + std.mem.indexOf(u8, result.stderr, "formatted") != null or + std.mem.indexOf(u8, result.stdout, "formatted") != null; + try testing.expect(has_format_msg); +} + +test "roc fmt --check succeeds on well-formatted file" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRoc(gpa, &.{ "fmt", "--check" }, "test/cli/well_formatted.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command succeeded (zero exit code) because file is well-formatted + try testing.expect(result.term == .Exited and result.term.Exited == 0); +} + +test "roc fmt reformats file in place" { + const testing = std.testing; + const gpa = testing.allocator; + + // Create a temporary copy of the unformatted file + var tmp_dir = testing.tmpDir(.{}); + var tmp = tmp_dir.dir; + defer tmp_dir.cleanup(); + + // Read the source file + const cwd = std.fs.cwd(); + const source_content = try cwd.readFileAlloc(gpa, "test/cli/needs_formatting.roc", 10 * 1024); + defer gpa.free(source_content); + const original_size = source_content.len; + + // Write to temp file + try tmp.writeFile(.{ .sub_path = "temp_format.roc", .data = source_content }); + + // Get absolute path to temp file + const tmp_path = try tmp.realpathAlloc(gpa, "."); + defer gpa.free(tmp_path); + const temp_file_path = try std.fs.path.join(gpa, &.{ tmp_path, "temp_format.roc" }); + defer gpa.free(temp_file_path); + + // Get absolute path to roc binary + const cwd_path = try cwd.realpathAlloc(gpa, "."); + defer gpa.free(cwd_path); + const roc_binary_name = if (@import("builtin").os.tag == .windows) "roc.exe" else "roc"; + const roc_path = try std.fs.path.join(gpa, &.{ cwd_path, "zig-out", "bin", roc_binary_name }); + defer gpa.free(roc_path); + + // Run roc fmt on the temp file + const result = try std.process.Child.run(.{ + .allocator = gpa, + .argv = &.{ roc_path, "fmt", temp_file_path }, + .cwd = cwd_path, + .max_output_bytes = 10 * 1024 * 1024, + }); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Read the formatted file + const formatted_content = try tmp.readFileAlloc(gpa, "temp_format.roc", 10 * 1024); + defer gpa.free(formatted_content); + const formatted_size = formatted_content.len; + + // Verify that: + // 1. The file size changed (formatting occurred) + try testing.expect(formatted_size != original_size); + + // 2. The formatted file is not empty + try testing.expect(formatted_size > 0); +} + +test "roc fmt does not change well-formatted file" { + const testing = std.testing; + const gpa = testing.allocator; + + // Read the well-formatted file before formatting + const cwd = std.fs.cwd(); + const before_content = try cwd.readFileAlloc(gpa, "test/cli/well_formatted.roc", 10 * 1024); + defer gpa.free(before_content); + + // Run roc fmt on the well-formatted file + const result = try util.runRoc(gpa, &.{"fmt"}, "test/cli/well_formatted.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Read the file after formatting + const after_content = try cwd.readFileAlloc(gpa, "test/cli/well_formatted.roc", 10 * 1024); + defer gpa.free(after_content); + + // Verify that the content is identical (file was not modified) + try testing.expectEqualStrings(before_content, after_content); +} + +test "roc fmt --stdin formats unformatted input" { + const testing = std.testing; + const gpa = testing.allocator; + + // Read the unformatted file to use as stdin + const cwd = std.fs.cwd(); + const input_content = try cwd.readFileAlloc(gpa, "test/cli/needs_formatting.roc", 10 * 1024); + defer gpa.free(input_content); + + // Get absolute path to roc binary + const cwd_path = try cwd.realpathAlloc(gpa, "."); + defer gpa.free(cwd_path); + const roc_binary_name = if (@import("builtin").os.tag == .windows) "roc.exe" else "roc"; + const roc_path = try std.fs.path.join(gpa, &.{ cwd_path, "zig-out", "bin", roc_binary_name }); + defer gpa.free(roc_path); + + // Skip test if roc binary doesn't exist + std.fs.accessAbsolute(roc_path, .{}) catch { + std.debug.print("Skipping test: roc binary not found at {s}\n", .{roc_path}); + }; + + // Run roc fmt --stdin with input piped in + var child = std.process.Child.init(&.{ roc_path, "fmt", "--stdin" }, gpa); + child.stdin_behavior = .Pipe; + child.stdout_behavior = .Pipe; + child.stderr_behavior = .Pipe; + child.cwd = cwd_path; + + try child.spawn(); + + // Write input to stdin and close it + try child.stdin.?.writeAll(input_content); + child.stdin.?.close(); + child.stdin = null; + + // Collect output before waiting + const stdout = try child.stdout.?.readToEndAlloc(gpa, 10 * 1024 * 1024); + defer gpa.free(stdout); + const stderr = try child.stderr.?.readToEndAlloc(gpa, 10 * 1024 * 1024); + defer gpa.free(stderr); + + // Wait for completion + const result = try child.wait(); + + // Verify that: + // 1. Command succeeded (zero exit code) + try testing.expect(result == .Exited and result.Exited == 0); + + // 2. Stdout contains formatted output (different from input) + try testing.expect(!std.mem.eql(u8, stdout, input_content)); + + // 3. Output is not empty + try testing.expect(stdout.len > 0); +} + +test "roc fmt --stdin does not change well-formatted input" { + const testing = std.testing; + const gpa = testing.allocator; + + // Read the well-formatted file to use as stdin + const cwd = std.fs.cwd(); + const input_content = try cwd.readFileAlloc(gpa, "test/cli/well_formatted.roc", 10 * 1024); + defer gpa.free(input_content); + + // Get absolute path to roc binary + const cwd_path = try cwd.realpathAlloc(gpa, "."); + defer gpa.free(cwd_path); + const roc_binary_name = if (@import("builtin").os.tag == .windows) "roc.exe" else "roc"; + const roc_path = try std.fs.path.join(gpa, &.{ cwd_path, "zig-out", "bin", roc_binary_name }); + defer gpa.free(roc_path); + + // Skip test if roc binary doesn't exist + std.fs.accessAbsolute(roc_path, .{}) catch { + std.debug.print("Skipping test: roc binary not found at {s}\n", .{roc_path}); + }; + + // Run roc fmt --stdin with input piped in + var child = std.process.Child.init(&.{ roc_path, "fmt", "--stdin" }, gpa); + child.stdin_behavior = .Pipe; + child.stdout_behavior = .Pipe; + child.stderr_behavior = .Pipe; + child.cwd = cwd_path; + + try child.spawn(); + + // Write input to stdin and close it + try child.stdin.?.writeAll(input_content); + child.stdin.?.close(); + child.stdin = null; + + // Collect output before waiting + const stdout = try child.stdout.?.readToEndAlloc(gpa, 10 * 1024 * 1024); + defer gpa.free(stdout); + const stderr = try child.stderr.?.readToEndAlloc(gpa, 10 * 1024 * 1024); + defer gpa.free(stderr); + + // Wait for completion + const result = try child.wait(); + + // Verify that: + // 1. Command succeeded (zero exit code) + try testing.expect(result == .Exited and result.Exited == 0); + + // 2. Stdout contains the same content as input (no changes) + try testing.expectEqualStrings(input_content, stdout); +} + +test "roc check reports type error - annotation mismatch" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRoc(gpa, &.{ "check", "--no-cache" }, "test/cli/has_type_error_annotation.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command failed (non-zero exit code) due to type error + try testing.expect(result.term != .Exited or result.term.Exited != 0); + + // 2. Stderr contains type error information + try testing.expect(result.stderr.len > 0); + + // 3. Error message mentions type mismatch or error + const has_type_error = std.mem.indexOf(u8, result.stderr, "TYPE MISMATCH") != null or + std.mem.indexOf(u8, result.stderr, "error") != null or + std.mem.indexOf(u8, result.stderr, "Found") != null; + try testing.expect(has_type_error); +} + +test "roc check reports type error - plus operator with incompatible types" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRoc(gpa, &.{ "check", "--no-cache" }, "test/cli/has_type_error_plus_operator.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command failed (non-zero exit code) due to type error + try testing.expect(result.term != .Exited or result.term.Exited != 0); + + // 2. Stderr contains type error information + try testing.expect(result.stderr.len > 0); + + // 3. Error message mentions missing method or type error + const has_type_error = std.mem.indexOf(u8, result.stderr, "MISSING METHOD") != null or + std.mem.indexOf(u8, result.stderr, "TYPE MISMATCH") != null or + std.mem.indexOf(u8, result.stderr, "error") != null or + std.mem.indexOf(u8, result.stderr, "Found") != null; + try testing.expect(has_type_error); +} + +test "roc test/int/app.roc runs successfully" { + // Skip on Windows - test/int platform doesn't have Windows host libraries + if (@import("builtin").os.tag == .windows) return error.SkipZigTest; + + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRoc(gpa, &.{"--no-cache"}, "test/int/app.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command succeeded (zero exit code) + try testing.expect(result.term == .Exited and result.term.Exited == 0); +} + +test "roc test/str/app.roc runs successfully" { + // Skip on Windows - test/str platform doesn't have Windows host libraries + if (@import("builtin").os.tag == .windows) return error.SkipZigTest; + + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRoc(gpa, &.{"--no-cache"}, "test/str/app.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command succeeded (zero exit code) + try testing.expect(result.term == .Exited and result.term.Exited == 0); +} + +// roc build tests + +test "roc build creates executable from test/int/app.roc" { + // Skip on Windows - test/int platform doesn't have Windows host libraries + if (@import("builtin").os.tag == .windows) return error.SkipZigTest; + + const testing = std.testing; + const gpa = testing.allocator; + + // Create a temp directory for the output + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const tmp_path = try tmp_dir.dir.realpathAlloc(gpa, "."); + defer gpa.free(tmp_path); + + const output_path = try std.fs.path.join(gpa, &.{ tmp_path, "test_app" }); + defer gpa.free(output_path); + + const output_arg = try std.fmt.allocPrint(gpa, "--output={s}", .{output_path}); + defer gpa.free(output_arg); + + const result = try util.runRoc(gpa, &.{ "build", output_arg }, "test/int/app.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command succeeded (zero exit code) + if (result.term != .Exited or result.term.Exited != 0) { + std.debug.print("roc build failed with exit code: {}\nstdout: {s}\nstderr: {s}\n", .{ result.term, result.stdout, result.stderr }); + } + try testing.expect(result.term == .Exited and result.term.Exited == 0); + + // 2. Output file was created + const stat = tmp_dir.dir.statFile("test_app") catch |err| { + std.debug.print("Failed to stat output file: {}\nstderr: {s}\n", .{ err, result.stderr }); + return err; + }; + + // 3. Output file is executable (non-zero size) + try testing.expect(stat.size > 0); + + // 4. Stdout contains success message + try testing.expect(result.stdout.len > 5); + try testing.expect(std.mem.indexOf(u8, result.stdout, "Successfully built") != null); +} + +test "roc build executable runs correctly" { + // Skip on Windows - test/int platform doesn't have Windows host libraries + if (@import("builtin").os.tag == .windows) return error.SkipZigTest; + + const testing = std.testing; + const gpa = testing.allocator; + + // Create a temp directory for the output + var tmp_dir = testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + const tmp_path = try tmp_dir.dir.realpathAlloc(gpa, "."); + defer gpa.free(tmp_path); + + const output_path = try std.fs.path.join(gpa, &.{ tmp_path, "test_app" }); + defer gpa.free(output_path); + + const output_arg = try std.fmt.allocPrint(gpa, "--output={s}", .{output_path}); + defer gpa.free(output_arg); + + // Build the app + const build_result = try util.runRoc(gpa, &.{ "build", output_arg }, "test/int/app.roc"); + defer gpa.free(build_result.stdout); + defer gpa.free(build_result.stderr); + + if (build_result.term != .Exited or build_result.term.Exited != 0) { + std.debug.print("roc build failed with exit code: {}\nstdout: {s}\nstderr: {s}\n", .{ build_result.term, build_result.stdout, build_result.stderr }); + } + try testing.expect(build_result.term == .Exited and build_result.term.Exited == 0); + + // Run the built executable + const run_result = try std.process.Child.run(.{ + .allocator = gpa, + .argv = &.{output_path}, + .max_output_bytes = 10 * 1024 * 1024, + }); + defer gpa.free(run_result.stdout); + defer gpa.free(run_result.stderr); + + // Verify that: + // 1. Executable ran successfully + try testing.expect(run_result.term == .Exited and run_result.term.Exited == 0); + + // 2. Output contains expected success message + const has_success = std.mem.indexOf(u8, run_result.stdout, "SUCCESS") != null or + std.mem.indexOf(u8, run_result.stdout, "PASSED") != null; + try testing.expect(has_success); +} + +test "roc build fails with file not found error" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRoc(gpa, &.{"build"}, "nonexistent_file.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command failed (non-zero exit code) + try testing.expect(result.term != .Exited or result.term.Exited != 0); + + // 2. Stderr contains file not found error + const has_error = std.mem.indexOf(u8, result.stderr, "FileNotFound") != null or + std.mem.indexOf(u8, result.stderr, "not found") != null or + std.mem.indexOf(u8, result.stderr, "NOT FOUND") != null or + std.mem.indexOf(u8, result.stderr, "Failed") != null; + try testing.expect(has_error); +} + +test "roc build fails with invalid target error" { + const testing = std.testing; + const gpa = testing.allocator; + + const result = try util.runRoc(gpa, &.{ "build", "--target=invalid_target_name" }, "test/int/app.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command failed (non-zero exit code) + try testing.expect(result.term != .Exited or result.term.Exited != 0); + + // 2. Stderr contains invalid target error + const has_error = std.mem.indexOf(u8, result.stderr, "Invalid target") != null or + std.mem.indexOf(u8, result.stderr, "invalid") != null; + try testing.expect(has_error); +} + +test "roc build glibc target gives helpful error on non-Linux" { + const testing = std.testing; + const builtin = @import("builtin"); + const gpa = testing.allocator; + + // This test only applies on non-Linux platforms + if (builtin.os.tag == .linux) { + return; // Skip on Linux where glibc cross-compilation is supported + } + + const result = try util.runRoc(gpa, &.{ "build", "--target=x64glibc" }, "test/int/app.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command failed (non-zero exit code) + try testing.expect(result.term != .Exited or result.term.Exited != 0); + + // 2. Stderr contains helpful error message about glibc not being supported + const has_glibc_error = std.mem.indexOf(u8, result.stderr, "glibc") != null; + try testing.expect(has_glibc_error); + + // 3. Stderr suggests using musl instead + const suggests_musl = std.mem.indexOf(u8, result.stderr, "musl") != null; + try testing.expect(suggests_musl); +} + +test "roc test with nested list chunks does not panic on layout upgrade" { + const testing = std.testing; + const gpa = testing.allocator; + + // This test verifies that nested list operations with layout upgrades + // (from list_of_zst to concrete list types) don't cause integer overflow panics. + // The expect in the test file is designed to fail, but execution should not panic. + const result = try util.runRoc(gpa, &.{"test"}, "test/cli/issue8699.roc"); + defer gpa.free(result.stdout); + defer gpa.free(result.stderr); + + // Verify that: + // 1. Command failed with exit code 1 (test failure, not panic) + try testing.expect(result.term == .Exited and result.term.Exited == 1); + + // 2. Stderr contains "FAIL" indicating a test failure (not a panic/crash) + const has_fail = std.mem.indexOf(u8, result.stderr, "FAIL") != null; + try testing.expect(has_fail); + + // 3. Stderr should not contain "panic" or "overflow" (no crash occurred) + const has_panic = std.mem.indexOf(u8, result.stderr, "panic") != null or + std.mem.indexOf(u8, result.stderr, "overflow") != null; + try testing.expect(!has_panic); +} diff --git a/src/cli/test/runner_core.zig b/src/cli/test/runner_core.zig new file mode 100644 index 0000000000..820b83484a --- /dev/null +++ b/src/cli/test/runner_core.zig @@ -0,0 +1,414 @@ +//! Shared execution logic for the test platform runner. +//! +//! This module provides common functions for: +//! - Cross-compilation of Roc apps +//! - Native build and execution +//! - Valgrind memory testing +//! - Result formatting and summary printing + +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; + +/// Result of a test execution +pub const TestResult = enum { + passed, + failed, + skipped, +}; + +/// Statistics for test run +pub const TestStats = struct { + passed: usize = 0, + failed: usize = 0, + skipped: usize = 0, + + pub fn total(self: TestStats) usize { + return self.passed + self.failed + self.skipped; + } + + pub fn record(self: *TestStats, result: TestResult) void { + switch (result) { + .passed => self.passed += 1, + .failed => self.failed += 1, + .skipped => self.skipped += 1, + } + } +}; + +/// Cross-compile a Roc app to a specific target. +/// Returns true if compilation succeeded. +pub fn crossCompile( + allocator: Allocator, + roc_binary: []const u8, + roc_file: []const u8, + target: []const u8, + output_name: []const u8, +) !TestResult { + const target_arg = try std.fmt.allocPrint(allocator, "--target={s}", .{target}); + defer allocator.free(target_arg); + + const output_arg = try std.fmt.allocPrint(allocator, "--output={s}", .{output_name}); + defer allocator.free(output_arg); + + const result = std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary, + "build", + target_arg, + output_arg, + roc_file, + }, + }) catch |err| { + std.debug.print("FAIL (spawn error: {})\n", .{err}); + return .failed; + }; + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + + return handleProcessResult(result, output_name); +} + +/// Build a Roc app natively (no cross-compilation). +/// Does NOT clean up the output file - caller is responsible for cleanup. +pub fn buildNative( + allocator: Allocator, + roc_binary: []const u8, + roc_file: []const u8, + output_name: []const u8, +) !TestResult { + const output_arg = try std.fmt.allocPrint(allocator, "--output={s}", .{output_name}); + defer allocator.free(output_arg); + + const result = std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary, + "build", + output_arg, + roc_file, + }, + }) catch |err| { + std.debug.print("FAIL (spawn error: {})\n", .{err}); + return .failed; + }; + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + + // Don't cleanup - caller will run and then cleanup + return handleProcessResultNoCleanup(result, output_name); +} + +/// Run a native executable and check for successful execution. +pub fn runNative( + allocator: Allocator, + exe_path: []const u8, +) !TestResult { + const result = std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{exe_path}, + }) catch |err| { + std.debug.print("FAIL (spawn error: {})\n", .{err}); + return .failed; + }; + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + + switch (result.term) { + .Exited => |code| { + if (code == 0) { + std.debug.print("OK\n", .{}); + // Print first few lines of output + if (result.stdout.len > 0) { + printTruncatedOutput(result.stdout, 3, " "); + } + return .passed; + } else { + std.debug.print("FAIL (exit code {d})\n", .{code}); + if (result.stderr.len > 0) { + printTruncatedOutput(result.stderr, 5, " "); + } + return .failed; + } + }, + .Signal => |sig| { + std.debug.print("FAIL (signal {d})\n", .{sig}); + return .failed; + }, + else => { + std.debug.print("FAIL (abnormal termination)\n", .{}); + return .failed; + }, + } +} + +/// Run a Roc app with --test mode and IO spec verification. +pub fn runWithIoSpec( + allocator: Allocator, + roc_binary: []const u8, + roc_file: []const u8, + io_spec: []const u8, +) !TestResult { + const test_arg = try std.fmt.allocPrint(allocator, "--test={s}", .{io_spec}); + defer allocator.free(test_arg); + + const result = std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + roc_binary, + "run", + test_arg, + roc_file, + }, + }) catch |err| { + std.debug.print("FAIL (spawn error: {})\n", .{err}); + return .failed; + }; + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + + // Check for GPA (General Purpose Allocator) errors in stderr + // These indicate memory bugs like alignment mismatches, double frees, etc. + if (std.mem.indexOf(u8, result.stderr, "error(gpa):") != null) { + std.debug.print("FAIL (memory error detected)\n", .{}); + printTruncatedOutput(result.stderr, 10, " "); + return .failed; + } + + switch (result.term) { + .Exited => |code| { + if (code == 0) { + std.debug.print("OK\n", .{}); + return .passed; + } else { + std.debug.print("FAIL (exit code {d})\n", .{code}); + if (result.stderr.len > 0) { + printTruncatedOutput(result.stderr, 5, " "); + } + return .failed; + } + }, + .Signal => |sig| { + std.debug.print("FAIL (signal {d})\n", .{sig}); + return .failed; + }, + else => { + std.debug.print("FAIL (abnormal termination)\n", .{}); + return .failed; + }, + } +} + +/// Run a Roc app under valgrind. +/// Only works on Linux x86_64. +pub fn runWithValgrind( + allocator: Allocator, + roc_binary: []const u8, + roc_file: []const u8, +) !TestResult { + // Valgrind only works on Linux x86_64 + if (builtin.os.tag != .linux or builtin.cpu.arch != .x86_64) { + std.debug.print("SKIP (valgrind requires Linux x86_64)\n", .{}); + return .skipped; + } + + const result = std.process.Child.run(.{ + .allocator = allocator, + .argv = &[_][]const u8{ + "./ci/custom_valgrind.sh", + roc_binary, + "--no-cache", + roc_file, + }, + }) catch |err| { + std.debug.print("FAIL (spawn error: {})\n", .{err}); + return .failed; + }; + defer allocator.free(result.stdout); + defer allocator.free(result.stderr); + + switch (result.term) { + .Exited => |code| { + if (code == 0) { + std.debug.print("OK\n", .{}); + return .passed; + } else { + std.debug.print("FAIL (valgrind exit code {d})\n", .{code}); + if (result.stderr.len > 0) { + printTruncatedOutput(result.stderr, 5, " "); + } + return .failed; + } + }, + .Signal => |sig| { + std.debug.print("FAIL (signal {d})\n", .{sig}); + return .failed; + }, + else => { + std.debug.print("FAIL (abnormal termination)\n", .{}); + return .failed; + }, + } +} + +/// Verify that required platform target files exist. +pub fn verifyPlatformFiles( + allocator: Allocator, + platform_dir: []const u8, + target: []const u8, +) !bool { + const libhost_path = try std.fmt.allocPrint(allocator, "{s}/platform/targets/{s}/libhost.a", .{ platform_dir, target }); + defer allocator.free(libhost_path); + + if (std.fs.cwd().access(libhost_path, .{})) |_| { + return true; + } else |_| { + return false; + } +} + +/// Check if a target requires Linux host (glibc targets). +pub fn requiresLinuxHost(target: []const u8) bool { + return std.mem.indexOf(u8, target, "glibc") != null; +} + +/// Check if we should skip this target on current host. +pub fn shouldSkipTarget(target: []const u8) bool { + if (requiresLinuxHost(target) and builtin.os.tag != .linux) { + return true; + } + return false; +} + +/// Clean up a generated file. +pub fn cleanup(path: []const u8) void { + std.fs.cwd().deleteFile(path) catch {}; +} + +/// Print a section header. +pub fn printHeader(comptime fmt: []const u8, args: anytype) void { + std.debug.print("\n>>> " ++ fmt ++ "\n", args); +} + +/// Print test summary. +pub fn printSummary(stats: TestStats) void { + std.debug.print("\n=== Summary ===\n", .{}); + std.debug.print("Passed: {d}\n", .{stats.passed}); + std.debug.print("Failed: {d}\n", .{stats.failed}); + std.debug.print("Skipped: {d}\n", .{stats.skipped}); + + if (stats.failed > 0) { + std.debug.print("\nSome tests failed!\n", .{}); + } else { + std.debug.print("\nAll tests passed!\n", .{}); + } +} + +/// Print a result line. +pub fn printResultLine(status: []const u8, target: []const u8, message: []const u8) void { + if (message.len > 0) { + std.debug.print("[{s}] {s} ({s})\n", .{ status, target, message }); + } else { + std.debug.print("[{s}] {s}\n", .{ status, target }); + } +} + +// --- Internal helpers --- + +fn handleProcessResult(result: std.process.Child.RunResult, output_name: []const u8) TestResult { + // Check for GPA (General Purpose Allocator) errors in stderr + // These indicate memory bugs like alignment mismatches, double frees, etc. + if (std.mem.indexOf(u8, result.stderr, "error(gpa):") != null) { + std.debug.print("FAIL (memory error detected)\n", .{}); + printTruncatedOutput(result.stderr, 10, " "); + cleanup(output_name); + return .failed; + } + + switch (result.term) { + .Exited => |code| { + if (code == 0) { + // Verify executable was created + if (std.fs.cwd().access(output_name, .{})) |_| { + std.debug.print("OK\n", .{}); + // Clean up + cleanup(output_name); + return .passed; + } else |_| { + std.debug.print("FAIL (executable not created)\n", .{}); + return .failed; + } + } else { + std.debug.print("FAIL (exit code {d})\n", .{code}); + if (result.stderr.len > 0) { + printTruncatedOutput(result.stderr, 5, " "); + } + return .failed; + } + }, + .Signal => |sig| { + std.debug.print("FAIL (signal {d})\n", .{sig}); + return .failed; + }, + else => { + std.debug.print("FAIL (abnormal termination)\n", .{}); + return .failed; + }, + } +} + +fn handleProcessResultNoCleanup(result: std.process.Child.RunResult, output_name: []const u8) TestResult { + // Check for GPA (General Purpose Allocator) errors in stderr + // These indicate memory bugs like alignment mismatches, double frees, etc. + if (std.mem.indexOf(u8, result.stderr, "error(gpa):") != null) { + std.debug.print("FAIL (memory error detected)\n", .{}); + printTruncatedOutput(result.stderr, 10, " "); + return .failed; + } + + switch (result.term) { + .Exited => |code| { + if (code == 0) { + // Verify executable was created + if (std.fs.cwd().access(output_name, .{})) |_| { + std.debug.print("OK\n", .{}); + // Don't clean up - caller will handle + return .passed; + } else |_| { + std.debug.print("FAIL (executable not created)\n", .{}); + return .failed; + } + } else { + std.debug.print("FAIL (exit code {d})\n", .{code}); + if (result.stderr.len > 0) { + printTruncatedOutput(result.stderr, 5, " "); + } + return .failed; + } + }, + .Signal => |sig| { + std.debug.print("FAIL (signal {d})\n", .{sig}); + return .failed; + }, + else => { + std.debug.print("FAIL (abnormal termination)\n", .{}); + return .failed; + }, + } +} + +fn printTruncatedOutput(output: []const u8, max_lines: usize, prefix: []const u8) void { + var lines = std.mem.splitScalar(u8, output, '\n'); + var line_count: usize = 0; + while (lines.next()) |line| { + if (line_count >= max_lines) { + std.debug.print("{s}... (truncated)\n", .{prefix}); + break; + } + if (line.len > 0) { + std.debug.print("{s}{s}\n", .{ prefix, line }); + line_count += 1; + } + } +} diff --git a/src/cli/test/test_runner.zig b/src/cli/test/test_runner.zig new file mode 100644 index 0000000000..d668b63eec --- /dev/null +++ b/src/cli/test/test_runner.zig @@ -0,0 +1,520 @@ +//! Unified test platform runner. +//! +//! This tool tests Roc test platforms with various modes: +//! - Cross-compilation to different targets +//! - Native build and execution +//! - Valgrind memory testing (Linux x86_64 only) +//! - IO spec verification (for fx platform) +//! +//! Usage: +//! test_runner [options] +//! +//! Platforms: +//! int - Integer operations platform +//! str - String processing platform +//! fx - Effectful platform (stdout/stderr/stdin) +//! fx-open - Effectful with open union errors +//! +//! Options: +//! --target= Target to test (default: all for platform) +//! Values: x64musl, arm64musl, x64glibc, arm64glibc, native +//! --mode= Test mode (default: all applicable) +//! Values: cross, native, valgrind +//! --verbose Show detailed output +//! +//! Examples: +//! test_runner ./zig-out/bin/roc int # All int tests +//! test_runner ./zig-out/bin/roc fx --target=x64musl # fx cross-compile to x64musl +//! test_runner ./zig-out/bin/roc str --mode=valgrind # str under valgrind +//! test_runner ./zig-out/bin/roc int --mode=native # int native only + +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; + +const platform_config = @import("platform_config.zig"); +const runner_core = @import("runner_core.zig"); +const fx_test_specs = @import("fx_test_specs.zig"); + +const PlatformConfig = platform_config.PlatformConfig; +const TestStats = runner_core.TestStats; +const TestResult = runner_core.TestResult; + +/// Test mode +const TestMode = enum { + cross, + native, + valgrind, + all, +}; + +/// Parsed command line arguments +const Args = struct { + roc_binary: []const u8, + platform_name: []const u8, + target_filter: ?[]const u8, + mode: TestMode, + verbose: bool, + /// Raw args buffer - caller must free via std.process.argsFree + raw_args: [][:0]u8, +}; + +/// Entry point for the unified test platform runner. +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + defer _ = gpa.deinit(); + const allocator = gpa.allocator(); + + const args = try parseArgs(allocator); + defer std.process.argsFree(allocator, args.raw_args); + + // Look up the platform + const platform = platform_config.findPlatform(args.platform_name) orelse { + std.debug.print("Error: Unknown platform '{s}'\n", .{args.platform_name}); + std.debug.print("Available platforms: int, str, fx, fx-open\n", .{}); + std.process.exit(1); + }; + + // Validate target if specified + if (args.target_filter) |target_name| { + if (!std.mem.eql(u8, target_name, "native")) { + if (platform_config.findTarget(platform, target_name) == null) { + std.debug.print("Error: Target '{s}' not supported by platform '{s}'\n", .{ target_name, platform.name }); + std.debug.print("Available targets: ", .{}); + for (platform.targets, 0..) |t, i| { + if (i > 0) std.debug.print(", ", .{}); + std.debug.print("{s}", .{t.name}); + } + std.debug.print(", native\n", .{}); + std.process.exit(1); + } + } + } + + // Print banner + std.debug.print("=== Test Platform Runner ===\n", .{}); + std.debug.print("Roc binary: {s}\n", .{args.roc_binary}); + std.debug.print("Platform: {s}\n", .{platform.name}); + if (args.target_filter) |t| { + std.debug.print("Target filter: {s}\n", .{t}); + } + std.debug.print("Mode: {s}\n", .{@tagName(args.mode)}); + std.debug.print("\n", .{}); + + var stats = TestStats{}; + + // Run tests based on mode + switch (args.mode) { + .cross => { + try runCrossCompileTests(allocator, args, platform, &stats); + }, + .native => { + try runNativeTests(allocator, args, platform, &stats); + }, + .valgrind => { + try runValgrindTests(allocator, args, platform, &stats); + }, + .all => { + // Run cross-compilation tests + try runCrossCompileTests(allocator, args, platform, &stats); + + // Run native tests + try runNativeTests(allocator, args, platform, &stats); + }, + } + + // Print summary + runner_core.printSummary(stats); + + if (stats.failed > 0) { + std.process.exit(1); + } +} + +fn runCrossCompileTests( + allocator: Allocator, + args: Args, + platform: PlatformConfig, + stats: *TestStats, +) !void { + runner_core.printHeader("Cross-compilation tests", .{}); + + // First verify platform files exist + std.debug.print("Verifying platform target files...\n", .{}); + var verify_failed = false; + + for (platform.targets) |target| { + // Apply target filter + if (args.target_filter) |filter| { + if (!std.mem.eql(u8, target.name, filter)) continue; + } + + // Skip glibc on non-Linux + if (runner_core.shouldSkipTarget(target.name)) { + runner_core.printResultLine("SKIP", target.name, "glibc requires Linux host"); + continue; + } + + const exists = try runner_core.verifyPlatformFiles(allocator, platform.base_dir, target.name); + if (exists) { + runner_core.printResultLine("OK", target.name, "libhost.a exists"); + } else { + runner_core.printResultLine("FAIL", target.name, "libhost.a missing"); + verify_failed = true; + } + } + + if (verify_failed) { + std.debug.print("\nPlatform verification failed. Aborting.\n", .{}); + std.process.exit(1); + } + + // Now run cross-compilation tests + std.debug.print("\n", .{}); + + switch (platform.test_apps) { + .single => |app_name| { + const roc_file = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ platform.base_dir, app_name }); + defer allocator.free(roc_file); + + for (platform.targets) |target| { + // Apply target filter + if (args.target_filter) |filter| { + if (!std.mem.eql(u8, target.name, filter)) continue; + } + + // Skip glibc on non-Linux + if (runner_core.shouldSkipTarget(target.name)) { + stats.record(.skipped); + continue; + } + + std.debug.print("Building {s} for {s}... ", .{ app_name, target.name }); + + const output_name = try std.fmt.allocPrint(allocator, "{s}_{s}", .{ platform.name, target.name }); + defer allocator.free(output_name); + + const result = try runner_core.crossCompile(allocator, args.roc_binary, roc_file, target.name, output_name); + stats.record(result); + } + }, + + .spec_list => |specs| { + for (platform.targets) |target| { + // Apply target filter + if (args.target_filter) |filter| { + if (!std.mem.eql(u8, target.name, filter)) continue; + } + + // Skip glibc on non-Linux + if (runner_core.shouldSkipTarget(target.name)) { + stats.record(.skipped); + continue; + } + + std.debug.print("Cross-compiling {d} tests for {s}...\n", .{ specs.len, target.name }); + + for (specs, 0..) |spec, i| { + const test_num = i + 1; + std.debug.print("[{d}/{d}] {s}... ", .{ test_num, specs.len, spec.roc_file }); + + const basename = std.fs.path.stem(spec.roc_file); + const output_name = try std.fmt.allocPrint(allocator, "{s}_{s}", .{ basename, target.name }); + defer allocator.free(output_name); + + const result = try runner_core.crossCompile(allocator, args.roc_binary, spec.roc_file, target.name, output_name); + stats.record(result); + } + } + }, + + .simple_list => |specs| { + for (platform.targets) |target| { + // Apply target filter + if (args.target_filter) |filter| { + if (!std.mem.eql(u8, target.name, filter)) continue; + } + + // Skip glibc on non-Linux + if (runner_core.shouldSkipTarget(target.name)) { + stats.record(.skipped); + continue; + } + + std.debug.print("Cross-compiling {d} tests for {s}...\n", .{ specs.len, target.name }); + + for (specs, 0..) |spec, i| { + const test_num = i + 1; + std.debug.print("[{d}/{d}] {s}... ", .{ test_num, specs.len, spec.roc_file }); + + const basename = std.fs.path.stem(spec.roc_file); + const output_name = try std.fmt.allocPrint(allocator, "{s}_{s}", .{ basename, target.name }); + defer allocator.free(output_name); + + const result = try runner_core.crossCompile(allocator, args.roc_binary, spec.roc_file, target.name, output_name); + stats.record(result); + } + } + }, + } +} + +fn runNativeTests( + allocator: Allocator, + args: Args, + platform: PlatformConfig, + stats: *TestStats, +) !void { + // Check if native target is filtered out + if (args.target_filter) |filter| { + if (!std.mem.eql(u8, filter, "native")) { + return; // Skip native tests if a specific cross target is requested + } + } + + if (!platform.supports_native_exec) { + return; + } + + runner_core.printHeader("Native build and execution tests", .{}); + + switch (platform.test_apps) { + .single => |app_name| { + const roc_file = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ platform.base_dir, app_name }); + defer allocator.free(roc_file); + + const output_name = try std.fmt.allocPrint(allocator, "{s}_native", .{platform.name}); + defer allocator.free(output_name); + + // Build + std.debug.print("Building {s} native... ", .{app_name}); + const build_result = try runner_core.buildNative(allocator, args.roc_binary, roc_file, output_name); + stats.record(build_result); + + if (build_result != .passed) { + return; + } + + // Run + std.debug.print("Running native executable... ", .{}); + const exe_path = try std.fmt.allocPrint(allocator, "./{s}", .{output_name}); + defer allocator.free(exe_path); + + const run_result = try runner_core.runNative(allocator, exe_path); + stats.record(run_result); + + // Cleanup + runner_core.cleanup(output_name); + }, + + .spec_list => |specs| { + if (platform.supports_io_specs) { + // Use IO spec verification + std.debug.print("Running {d} IO spec tests...\n", .{specs.len}); + + for (specs, 0..) |spec, i| { + const test_num = i + 1; + std.debug.print("[{d}/{d}] {s}... ", .{ test_num, specs.len, spec.roc_file }); + + const result = try runner_core.runWithIoSpec(allocator, args.roc_binary, spec.roc_file, spec.io_spec); + stats.record(result); + } + } else { + // Just build and run each test + for (specs, 0..) |spec, i| { + const test_num = i + 1; + const basename = std.fs.path.stem(spec.roc_file); + const output_name = try std.fmt.allocPrint(allocator, "{s}_native", .{basename}); + defer allocator.free(output_name); + + std.debug.print("[{d}/{d}] Building {s}... ", .{ test_num, specs.len, spec.roc_file }); + const build_result = try runner_core.buildNative(allocator, args.roc_binary, spec.roc_file, output_name); + stats.record(build_result); + + if (build_result == .passed) { + const exe_path = try std.fmt.allocPrint(allocator, "./{s}", .{output_name}); + defer allocator.free(exe_path); + + std.debug.print(" Running... ", .{}); + const run_result = try runner_core.runNative(allocator, exe_path); + stats.record(run_result); + + runner_core.cleanup(output_name); + } + } + } + }, + + .simple_list => |specs| { + // Build and run each test (no IO spec verification) + std.debug.print("Running {d} native tests...\n", .{specs.len}); + + for (specs, 0..) |spec, i| { + const test_num = i + 1; + const basename = std.fs.path.stem(spec.roc_file); + const output_name = try std.fmt.allocPrint(allocator, "{s}_native", .{basename}); + defer allocator.free(output_name); + + std.debug.print("[{d}/{d}] Building {s}... ", .{ test_num, specs.len, spec.roc_file }); + const build_result = try runner_core.buildNative(allocator, args.roc_binary, spec.roc_file, output_name); + stats.record(build_result); + + if (build_result == .passed) { + const exe_path = try std.fmt.allocPrint(allocator, "./{s}", .{output_name}); + defer allocator.free(exe_path); + + std.debug.print(" Running... ", .{}); + const run_result = try runner_core.runNative(allocator, exe_path); + stats.record(run_result); + + runner_core.cleanup(output_name); + } + } + }, + } +} + +fn runValgrindTests( + allocator: Allocator, + args: Args, + platform: PlatformConfig, + stats: *TestStats, +) !void { + // Valgrind only works on Linux x86_64 + if (builtin.os.tag != .linux or builtin.cpu.arch != .x86_64) { + std.debug.print("Skipping valgrind tests (requires Linux x86_64)\n", .{}); + return; + } + + if (!platform.valgrind_safe) { + std.debug.print("Skipping valgrind tests for {s} (has stdin tests)\n", .{platform.name}); + return; + } + + runner_core.printHeader("Valgrind memory tests", .{}); + + switch (platform.test_apps) { + .single => |app_name| { + const roc_file = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ platform.base_dir, app_name }); + defer allocator.free(roc_file); + + std.debug.print("Running {s} under valgrind... ", .{app_name}); + const result = try runner_core.runWithValgrind(allocator, args.roc_binary, roc_file); + stats.record(result); + }, + + .spec_list => |specs| { + // For valgrind, only run tests that don't use stdin + var valgrind_safe_count: usize = 0; + for (specs) |spec| { + if (std.mem.indexOf(u8, spec.io_spec, "0<") == null) { + valgrind_safe_count += 1; + } + } + + std.debug.print("Running {d} valgrind-safe tests...\n", .{valgrind_safe_count}); + + var test_num: usize = 0; + for (specs) |spec| { + // Skip tests that use stdin + if (std.mem.indexOf(u8, spec.io_spec, "0<") != null) { + continue; + } + + test_num += 1; + std.debug.print("[{d}/{d}] {s}... ", .{ test_num, valgrind_safe_count, spec.roc_file }); + const result = try runner_core.runWithValgrind(allocator, args.roc_binary, spec.roc_file); + stats.record(result); + } + }, + + .simple_list => |specs| { + // All simple tests are valgrind-safe (no stdin) + std.debug.print("Running {d} valgrind tests...\n", .{specs.len}); + + for (specs, 0..) |spec, i| { + const test_num = i + 1; + std.debug.print("[{d}/{d}] {s}... ", .{ test_num, specs.len, spec.roc_file }); + const result = try runner_core.runWithValgrind(allocator, args.roc_binary, spec.roc_file); + stats.record(result); + } + }, + } +} + +fn parseArgs(allocator: Allocator) !Args { + const raw_args = try std.process.argsAlloc(allocator); + + if (raw_args.len < 3) { + printUsage(); + std.process.exit(1); + } + + var args = Args{ + .roc_binary = raw_args[1], + .platform_name = raw_args[2], + .target_filter = null, + .mode = .all, + .verbose = false, + .raw_args = raw_args, + }; + + // Parse options + var i: usize = 3; + while (i < raw_args.len) : (i += 1) { + const arg = raw_args[i]; + + if (std.mem.startsWith(u8, arg, "--target=")) { + args.target_filter = arg["--target=".len..]; + } else if (std.mem.startsWith(u8, arg, "--mode=")) { + const mode_str = arg["--mode=".len..]; + if (std.mem.eql(u8, mode_str, "cross")) { + args.mode = .cross; + } else if (std.mem.eql(u8, mode_str, "native")) { + args.mode = .native; + } else if (std.mem.eql(u8, mode_str, "valgrind")) { + args.mode = .valgrind; + } else if (std.mem.eql(u8, mode_str, "all")) { + args.mode = .all; + } else { + std.debug.print("Error: Unknown mode '{s}'\n", .{mode_str}); + std.debug.print("Available modes: cross, native, valgrind, all\n", .{}); + std.process.exit(1); + } + } else if (std.mem.eql(u8, arg, "--verbose")) { + args.verbose = true; + } else { + std.debug.print("Error: Unknown option '{s}'\n", .{arg}); + printUsage(); + std.process.exit(1); + } + } + + return args; +} + +fn printUsage() void { + std.debug.print( + \\Usage: test_runner [options] + \\ + \\Platforms: + \\ int - Integer operations platform + \\ str - String processing platform + \\ fx - Effectful platform (stdout/stderr/stdin) + \\ fx-open - Effectful with open union errors + \\ + \\Options: + \\ --target= Target to test (default: all for platform) + \\ Values: x64musl, arm64musl, x64glibc, arm64glibc, native + \\ --mode= Test mode (default: all applicable) + \\ Values: cross, native, valgrind, all + \\ --verbose Show detailed output + \\ + \\Examples: + \\ test_runner ./zig-out/bin/roc int # All int tests + \\ test_runner ./zig-out/bin/roc fx --target=x64musl # fx cross-compile to x64musl + \\ test_runner ./zig-out/bin/roc str --mode=valgrind # str under valgrind + \\ test_runner ./zig-out/bin/roc int --mode=native # int native only + \\ + , .{}); +} diff --git a/src/cli/test/util.zig b/src/cli/test/util.zig new file mode 100644 index 0000000000..0b71eb34f1 --- /dev/null +++ b/src/cli/test/util.zig @@ -0,0 +1,87 @@ +//! Utilities for CLI tests using the actual roc binary. + +const std = @import("std"); + +/// Result of executing a Roc command during testing. +/// Contains the captured output streams and process termination status. +pub const RocResult = struct { + stdout: []u8, + stderr: []u8, + term: std.process.Child.Term, +}; + +/// Helper to run roc with arguments that don't require a test file +pub fn runRocCommand(allocator: std.mem.Allocator, args: []const []const u8) !RocResult { + // Get absolute path to roc binary from current working directory + const cwd_path = try std.fs.cwd().realpathAlloc(allocator, "."); + defer allocator.free(cwd_path); + const roc_binary_name = if (@import("builtin").os.tag == .windows) "roc.exe" else "roc"; + const roc_path = try std.fs.path.join(allocator, &.{ cwd_path, "zig-out", "bin", roc_binary_name }); + defer allocator.free(roc_path); + + // Skip test if roc binary doesn't exist + std.fs.accessAbsolute(roc_path, .{}) catch { + std.debug.print("Skipping test: roc binary not found at {s}\n", .{roc_path}); + }; + + // Build argv: [roc_path, ...args] + const argv = try std.mem.concat(allocator, []const u8, &.{ + &.{roc_path}, + args, + }); + defer allocator.free(argv); + + // Run roc and capture output + const result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = argv, + .cwd = cwd_path, + .max_output_bytes = 10 * 1024 * 1024, // 10MB + }); + + return RocResult{ + .stdout = result.stdout, + .stderr = result.stderr, + .term = result.term, + }; +} + +/// Helper to set up and run roc with arbitrary arguments +pub fn runRoc(allocator: std.mem.Allocator, args: []const []const u8, test_file_path: []const u8) !RocResult { + // Get absolute path to roc binary from current working directory + const cwd_path = try std.fs.cwd().realpathAlloc(allocator, "."); + defer allocator.free(cwd_path); + const roc_binary_name = if (@import("builtin").os.tag == .windows) "roc.exe" else "roc"; + const roc_path = try std.fs.path.join(allocator, &.{ cwd_path, "zig-out", "bin", roc_binary_name }); + defer allocator.free(roc_path); + + // Skip test if roc binary doesn't exist + std.fs.accessAbsolute(roc_path, .{}) catch { + std.debug.print("Skipping test: roc binary not found at {s}\n", .{roc_path}); + }; + + const test_file = try std.fs.path.join(allocator, &.{ cwd_path, test_file_path }); + defer allocator.free(test_file); + + // Build argv: [roc_path, ...args, test_file] + const argv = try std.mem.concat(allocator, []const u8, &.{ + &.{roc_path}, + args, + &.{test_file}, + }); + defer allocator.free(argv); + + // Run roc and capture output + const result = try std.process.Child.run(.{ + .allocator = allocator, + .argv = argv, + .cwd = cwd_path, + .max_output_bytes = 10 * 1024 * 1024, // 10MB + }); + + return RocResult{ + .stdout = result.stdout, + .stderr = result.stderr, + .term = result.term, + }; +} diff --git a/src/cli/test_bundle_logic.zig b/src/cli/test_bundle_logic.zig deleted file mode 100644 index 172ee90d08..0000000000 --- a/src/cli/test_bundle_logic.zig +++ /dev/null @@ -1,132 +0,0 @@ -//! Unit tests for the bundle CLI logic (sorting, deduplication, first arg preservation) - -const std = @import("std"); -const testing = std.testing; - -test "bundle paths - empty list defaults to main.roc" { - // Simulate the logic from rocBundle - const paths_to_use = if (0 == 0) &[_][]const u8{"main.roc"} else &[_][]const u8{}; - - try testing.expectEqual(@as(usize, 1), paths_to_use.len); - try testing.expectEqualStrings("main.roc", paths_to_use[0]); -} - -test "bundle paths - single file unchanged" { - const allocator = testing.allocator; - - var file_paths = std.ArrayList([]const u8).init(allocator); - defer file_paths.deinit(); - - try file_paths.append("app.roc"); - - try testing.expectEqual(@as(usize, 1), file_paths.items.len); - try testing.expectEqualStrings("app.roc", file_paths.items[0]); -} - -test "bundle paths - sorting and deduplication" { - const allocator = testing.allocator; - - var file_paths = std.ArrayList([]const u8).init(allocator); - defer file_paths.deinit(); - - // Add paths in non-sorted order with duplicates - try file_paths.append("zebra.roc"); - try file_paths.append("apple.roc"); - try file_paths.append("banana.roc"); - try file_paths.append("apple.roc"); - - const first_cli_path = file_paths.items[0]; // "zebra.roc" - - // Sort - std.mem.sort([]const u8, file_paths.items, {}, struct { - fn lessThan(_: void, a: []const u8, b: []const u8) bool { - return std.mem.order(u8, a, b) == .lt; - } - }.lessThan); - - // Remove duplicates - var unique_count: usize = 0; - for (file_paths.items, 0..) |path, i| { - if (i == 0 or !std.mem.eql(u8, path, file_paths.items[i - 1])) { - file_paths.items[unique_count] = path; - unique_count += 1; - } - } - file_paths.items.len = unique_count; - - // Verify deduplication worked - try testing.expectEqual(@as(usize, 3), file_paths.items.len); - - // Ensure first CLI path stays first - if (file_paths.items.len > 1) { - var found_index: ?usize = null; - for (file_paths.items, 0..) |path, i| { - if (std.mem.eql(u8, path, first_cli_path)) { - found_index = i; - break; - } - } - - if (found_index) |idx| { - if (idx != 0) { - const temp = file_paths.items[0]; - file_paths.items[0] = file_paths.items[idx]; - file_paths.items[idx] = temp; - } - } - } - - // Verify final order - try testing.expectEqualStrings("zebra.roc", file_paths.items[0]); - try testing.expectEqualStrings("banana.roc", file_paths.items[1]); - try testing.expectEqualStrings("apple.roc", file_paths.items[2]); -} - -test "bundle paths - preserves first CLI arg with many files" { - const allocator = testing.allocator; - - var file_paths = std.ArrayList([]const u8).init(allocator); - defer file_paths.deinit(); - - // Add 8 paths with specific first - try file_paths.append("tests/test2.roc"); - try file_paths.append("main.roc"); - try file_paths.append("src/app.roc"); - try file_paths.append("src/lib.roc"); - try file_paths.append("src/utils/helper.roc"); - try file_paths.append("tests/test1.roc"); - try file_paths.append("docs/readme.md"); - try file_paths.append("config.roc"); - - const first_cli_path = file_paths.items[0]; // "tests/test2.roc" - - // Sort - std.mem.sort([]const u8, file_paths.items, {}, struct { - fn lessThan(_: void, a: []const u8, b: []const u8) bool { - return std.mem.order(u8, a, b) == .lt; - } - }.lessThan); - - // Find and move first CLI path to front - if (file_paths.items.len > 1) { - var found_index: ?usize = null; - for (file_paths.items, 0..) |path, i| { - if (std.mem.eql(u8, path, first_cli_path)) { - found_index = i; - break; - } - } - - if (found_index) |idx| { - if (idx != 0) { - const temp = file_paths.items[0]; - file_paths.items[0] = file_paths.items[idx]; - file_paths.items[idx] = temp; - } - } - } - - // Verify first path is preserved - try testing.expectEqualStrings("tests/test2.roc", file_paths.items[0]); - try testing.expectEqual(@as(usize, 8), file_paths.items.len); -} diff --git a/src/cli/test_docs.zig b/src/cli/test_docs.zig new file mode 100644 index 0000000000..2cca1198a4 --- /dev/null +++ b/src/cli/test_docs.zig @@ -0,0 +1,341 @@ +//! TODO +const std = @import("std"); +const testing = std.testing; +const main = @import("main.zig"); +const cli_args = @import("cli_args.zig"); + +test "roc docs generates nested package documentation" { + const gpa = testing.allocator; + + // Create a temporary directory for our test + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + + const tmp_path = try tmp.dir.realpathAlloc(gpa, "."); + defer gpa.free(tmp_path); + + // Create nested package structure: + // root (app) -> depends on foo, bar + // foo (package) -> depends on baz + // baz (package) -> depends on qux + // qux (package) -> no deps + // bar (package) -> depends on baz (shared dependency) + + // Create qux package (leaf dependency) + try tmp.dir.makeDir("qux"); + const qux_file = try tmp.dir.createFile("qux/main.roc", .{}); + defer qux_file.close(); + try qux_file.writeAll( + \\package [add] {} + \\ + \\add : I64, I64 -> I64 + \\add = \a, b -> a + b + \\ + ); + + // Create baz package (depends on qux) + try tmp.dir.makeDir("baz"); + const baz_file = try tmp.dir.createFile("baz/main.roc", .{}); + defer baz_file.close(); + try baz_file.writeAll( + \\package [multiply] { qux: "../qux/main.roc" } + \\ + \\multiply : I64, I64 -> I64 + \\multiply = \a, b -> a * b + \\ + ); + + // Create foo package (depends on baz) + try tmp.dir.makeDir("foo"); + const foo_file = try tmp.dir.createFile("foo/main.roc", .{}); + defer foo_file.close(); + try foo_file.writeAll( + \\package [increment] { baz: "../baz/main.roc" } + \\ + \\increment : I64 -> I64 + \\increment = \n -> n + 1 + \\ + ); + + // Create bar package (also depends on baz - shared dependency) + try tmp.dir.makeDir("bar"); + const bar_file = try tmp.dir.createFile("bar/main.roc", .{}); + defer bar_file.close(); + try bar_file.writeAll( + \\package [decrement] { baz: "../baz/main.roc" } + \\ + \\decrement : I64 -> I64 + \\decrement = \n -> n - 1 + \\ + ); + + // Create root app (depends on foo and bar) + const root_file = try tmp.dir.createFile("root.roc", .{}); + defer root_file.close(); + try root_file.writeAll( + \\app [main] { + \\ pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.19.0/Hj-J_zxz-8W4-Or3AUfyRJHhxUlgwBXNHkJcUqZPnPg.tar.br", + \\ foo: "foo/main.roc", + \\ bar: "bar/main.roc", + \\} + \\ + \\import pf.Stdout + \\ + \\main = + \\ Stdout.line! "Hello" + \\ + ); + + // Note: We would call main.rocDocs(gpa, args) here, but it requires + // a full build environment setup. Instead, we test the individual + // helper functions in separate tests below. + + // For now, just verify the test structure was created correctly + tmp.dir.access("root.roc", .{}) catch unreachable; + tmp.dir.access("foo/main.roc", .{}) catch unreachable; + tmp.dir.access("bar/main.roc", .{}) catch unreachable; + tmp.dir.access("baz/main.roc", .{}) catch unreachable; + tmp.dir.access("qux/main.roc", .{}) catch unreachable; +} + +test "generatePackageIndex creates valid HTML" { + const gpa = testing.allocator; + + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + + const tmp_path = try tmp.dir.realpathAlloc(gpa, "."); + defer gpa.free(tmp_path); + + // Test with no dependencies or modules + { + const shorthands = [_][]const u8{}; + const modules = [_]main.ModuleInfo{}; + try main.generatePackageIndex(gpa, tmp_path, "test/module.roc", &shorthands, &modules); + + const content = try tmp.dir.readFileAlloc(gpa, "index.html", 10000); + defer gpa.free(content); + + try testing.expect(std.mem.indexOf(u8, content, "

    test/module.roc

    ") != null); + try testing.expect(std.mem.indexOf(u8, content, "") != null); + try testing.expect(std.mem.indexOf(u8, content, "") != null); + } + + // Delete the file for next test + try tmp.dir.deleteFile("index.html"); + + // Test with package dependencies + { + const shorthands = [_][]const u8{ "foo", "bar", "baz" }; + const modules = [_]main.ModuleInfo{}; + try main.generatePackageIndex(gpa, tmp_path, "root.roc", &shorthands, &modules); + + const content = try tmp.dir.readFileAlloc(gpa, "index.html", 10000); + defer gpa.free(content); + + try testing.expect(std.mem.indexOf(u8, content, "

    root.roc

    ") != null); + try testing.expect(std.mem.indexOf(u8, content, "
      ") != null); + try testing.expect(std.mem.indexOf(u8, content, "foo") != null); + try testing.expect(std.mem.indexOf(u8, content, "bar") != null); + try testing.expect(std.mem.indexOf(u8, content, "baz") != null); + } +} + +test "generatePackageIndex with imported modules" { + const gpa = testing.allocator; + + var tmp = testing.tmpDir(.{}); + defer tmp.cleanup(); + + const tmp_path = try tmp.dir.realpathAlloc(gpa, "."); + defer gpa.free(tmp_path); + + // Test with local and package modules + { + const shorthands = [_][]const u8{"pf"}; + + const empty_items = [_]main.AssociatedItem{}; + var modules = [_]main.ModuleInfo{ + .{ .name = try gpa.dupe(u8, "Foo"), .link_path = try gpa.dupe(u8, "Foo"), .associated_items = &empty_items }, + .{ .name = try gpa.dupe(u8, "Bar"), .link_path = try gpa.dupe(u8, "Bar"), .associated_items = &empty_items }, + .{ .name = try gpa.dupe(u8, "pf.Stdout"), .link_path = try gpa.dupe(u8, "pf/Stdout"), .associated_items = &empty_items }, + }; + defer for (modules) |mod| mod.deinit(gpa); + + try main.generatePackageIndex(gpa, tmp_path, "root.roc", &shorthands, &modules); + + const content = try tmp.dir.readFileAlloc(gpa, "index.html", 10000); + defer gpa.free(content); + + // Check for sidebar + try testing.expect(std.mem.indexOf(u8, content, "