diff --git a/.github/actions/flaky-retry/action.yml b/.github/actions/flaky-retry/action.yml index b72b3cf0a7..60174f38e2 100644 --- a/.github/actions/flaky-retry/action.yml +++ b/.github/actions/flaky-retry/action.yml @@ -28,8 +28,11 @@ runs: 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 @@ -95,4 +98,4 @@ runs: Write-Host "Not retrying" exit $exitCode } - } \ No newline at end of file + } 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_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 index 729895274c..559edc0b6a 100644 --- a/.github/workflows/ci_cross_compile.yml +++ b/.github/workflows/ci_cross_compile.yml @@ -15,7 +15,7 @@ jobs: matrix: host: [ ubuntu-22.04, # Linux x64 host - macos-13, # macOS x64 host + macos-15-intel, # macOS x64 host macos-15, # macOS ARM64 host windows-2022, # Windows x64 host ] diff --git a/.github/workflows/ci_manager.yml b/.github/workflows/ci_manager.yml index 14c437252d..928aa12702 100644 --- a/.github/workflows/ci_manager.yml +++ b/.github/workflows/ci_manager.yml @@ -32,6 +32,7 @@ jobs: - 'build.zig.zon' - '.github/workflows/ci_zig.yml' - '.github/workflows/ci_cross_compile.yml' + - '.github/actions/flaky-retry/action.yml' - 'ci/zig_lints.sh' - uses: dorny/paths-filter@v3 id: other_filter @@ -48,6 +49,7 @@ jobs: - '!build.zig.zon' - '!.github/workflows/ci_zig.yml' - '!.github/workflows/ci_cross_compile.yml' + - '!.github/actions/flaky-retry/action.yml' - '!ci/zig_lints.sh' # Files that ci manager workflows should not run on. - '!.gitignore' diff --git a/.github/workflows/ci_zig.yml b/.github/workflows/ci_zig.yml index c4ec973df8..cc137091c9 100644 --- a/.github/workflows/ci_zig.yml +++ b/.github/workflows/ci_zig.yml @@ -62,7 +62,7 @@ jobs: - name: Run Playground Tests run: | - zig build playground-test -Doptimize=ReleaseSmall -- --verbose + zig build test-playground -Doptimize=ReleaseSmall -- --verbose zig-tests: needs: check-once @@ -71,14 +71,14 @@ jobs: fail-fast: false matrix: os: [ - macos-13, + macos-15-intel, macos-15, ubuntu-22.04, ubuntu-24.04-arm, windows-2022, windows-2025, windows-11-arm, - ] # macos-13 uses x64, macos-15 uses arm64 + ] # macos-15 uses arm64 steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4 @@ -97,7 +97,7 @@ jobs: run: | sudo rm /usr/lib/llvm-18/bin/llvm-config - - name: build roc + - name: build repro executables run: | zig build -Dfuzz -Dsystem-afl=false @@ -137,9 +137,12 @@ jobs: - name: zig snapshot tests run: zig build snapshot -- --debug - - name: zig tests - run: | - zig build test -Dfuzz -Dsystem-afl=false + - name: build repro executables + uses: ./.github/actions/flaky-retry + with: + command: 'zig build test -Dfuzz -Dsystem-afl=false' + error_string_contains: 'double roundtrip bundle' + retry_count: 3 - name: Check for snapshot changes run: | diff --git a/.github/workflows/nightly_linux_arm64.yml b/.github/workflows/nightly_linux_arm64.yml index 9d3734e0db..cb5279c496 100644 --- a/.github/workflows/nightly_linux_arm64.yml +++ b/.github/workflows/nightly_linux_arm64.yml @@ -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/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 410673cc31..5e5c00bacc 100644 --- a/.github/workflows/test_alpha_many_os.yml +++ b/.github/workflows/test_alpha_many_os.yml @@ -9,11 +9,11 @@ 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: @@ -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 c38d1e9356..86627f1a7c 100644 --- a/.github/workflows/test_nightly_many_os.yml +++ b/.github/workflows/test_nightly_many_os.yml @@ -9,11 +9,11 @@ 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: @@ -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/build.zig b/build.zig index d8ded9d50a..6715b19757 100644 --- a/build.zig +++ b/build.zig @@ -19,12 +19,23 @@ pub fn build(b: *std.Build) void { 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"); // 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 no_bin = b.option(bool, "no-bin", "Skip emitting binaries (important for fast incremental compilation)") orelse false; @@ -79,11 +90,168 @@ 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; roc_modules.addAll(roc_exe); install_and_run(b, no_bin, roc_exe, roc_step, run_step, run_args); + // 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. + + // Discover all .roc files in src/build/roc/ + const roc_files = discoverBuiltinRocFiles(b) catch |err| { + std.debug.print("Failed to discover builtin .roc files: {}\n", .{err}); + return; + }; + + // Check if we need to rebuild builtins by comparing .roc and .bin file timestamps + const should_rebuild_builtins = blk: { + for (roc_files) |roc_path| { + // Get the base name (e.g., "Dict" from "src/build/roc/Dict.roc") + const roc_basename = std.fs.path.basename(roc_path); + const name_without_ext = roc_basename[0 .. roc_basename.len - 4]; // Remove ".roc" + + // Check if corresponding .bin file exists and is up-to-date + const bin_path = b.fmt("zig-out/builtins/{s}.bin", .{name_without_ext}); + + const roc_stat = std.fs.cwd().statFile(roc_path) catch break :blk true; + const bin_stat = std.fs.cwd().statFile(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; + } + } + + // All .bin files exist and are up-to-date + break :blk false; + }; + + const write_compiled_builtins = b.addWriteFiles(); + + if (should_rebuild_builtins) { + // Build and run the compiler + const builtin_compiler_exe = b.addExecutable(.{ + .name = "builtin_compiler", + .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) + }); + + // 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)); + } + + write_compiled_builtins.step.dependOn(&run_builtin_compiler.step); + + // Copy all generated .bin files from zig-out to build cache + 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]; + const bin_filename = b.fmt("{s}.bin", .{name_without_ext}); + + _ = write_compiled_builtins.addCopyFile( + .{ .cwd_relative = b.fmt("zig-out/builtins/{s}", .{bin_filename}) }, + bin_filename, + ); + } + } else { + // Use existing .bin files from zig-out/builtins/ + 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]; + const bin_filename = b.fmt("{s}.bin", .{name_without_ext}); + + _ = write_compiled_builtins.addCopyFile( + .{ .cwd_relative = b.fmt("zig-out/builtins/{s}", .{bin_filename}) }, + bin_filename, + ); + } + } + + // Generate compiled_builtins.zig dynamically based on discovered .roc files + const builtins_source_str = generateCompiledBuiltinsSource(b, roc_files) catch |err| { + std.debug.print("Failed to generate compiled_builtins.zig: {}\n", .{err}); + return; + }; + + 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, + }); + + // 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)", + ); + + // 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; + }; + + // Always build and run the compiler for this command + const builtin_compiler_exe_force = b.addExecutable(.{ + .name = "builtin_compiler", + .root_source_file = b.path("src/build/builtin_compiler/main.zig"), + .target = b.graph.host, + .optimize = .Debug, + }); + + builtin_compiler_exe_force.root_module.addImport("base", roc_modules.base); + builtin_compiler_exe_force.root_module.addImport("collections", roc_modules.collections); + builtin_compiler_exe_force.root_module.addImport("types", roc_modules.types); + builtin_compiler_exe_force.root_module.addImport("parse", roc_modules.parse); + builtin_compiler_exe_force.root_module.addImport("can", roc_modules.can); + builtin_compiler_exe_force.root_module.addImport("check", roc_modules.check); + builtin_compiler_exe_force.root_module.addImport("reporting", roc_modules.reporting); + builtin_compiler_exe_force.root_module.addImport("builtins", roc_modules.builtins); + + add_tracy(b, roc_modules.build_options, builtin_compiler_exe_force, b.graph.host, false, flag_enable_tracy); + + const run_builtin_compiler_force = b.addRunArtifact(builtin_compiler_exe_force); + + // Add all discovered .roc files as inputs + for (roc_files_force) |roc_path| { + run_builtin_compiler_force.addFileArg(b.path(roc_path)); + } + + 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", @@ -95,6 +263,8 @@ pub fn build(b: *std.Build) void { }), }); 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, run_args); @@ -155,84 +325,151 @@ 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_source_file = b.path("test/serialization_size_check.zig"), + .target = target, + .optimize = .Debug, + }); + 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_source_file = b.path("test/serialization_size_check.zig"), + .target = b.resolveTargetQuery(.{ + .cpu_arch = .wasm32, + .os_tag = .freestanding, + }), + .optimize = .Debug, + }); + 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); + } + // Create and add module tests const module_tests = roc_modules.createModuleTests(b, target, optimize, zstd, test_filters); for (module_tests) |module_test| { + // Add compiled builtins to check module tests + if (std.mem.eql(u8, module_test.test_step.name, "check")) { + 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); } // Add snapshot tool test - 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); - 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); - if (run_args.len != 0) { - run_snapshot_test.addArgs(run_args); + const run_snapshot_test = b.addRunArtifact(snapshot_test); + if (run_args.len != 0) { + run_snapshot_test.addArgs(run_args); + } + test_step.dependOn(&run_snapshot_test.step); } - test_step.dependOn(&run_snapshot_test.step); // Add CLI test - 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); + 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); - const run_cli_test = b.addRunArtifact(cli_test); - if (run_args.len != 0) { - run_cli_test.addArgs(run_args); + const run_cli_test = b.addRunArtifact(cli_test); + if (run_args.len != 0) { + run_cli_test.addArgs(run_args); + } + test_step.dependOn(&run_cli_test.step); } - test_step.dependOn(&run_cli_test.step); // Add 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); + 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) { - watch_test.linkFramework("CoreFoundation"); - watch_test.linkFramework("CoreServices"); - } else if (target.result.os.tag == .windows) { - watch_test.linkSystemLibrary("kernel32"); - } + // 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_watch_test = b.addRunArtifact(watch_test); - if (run_args.len != 0) { - run_watch_test.addArgs(run_args); + const run_watch_test = b.addRunArtifact(watch_test); + if (run_args.len != 0) { + run_watch_test.addArgs(run_args); + } + test_step.dependOn(&run_watch_test.step); } - test_step.dependOn(&run_watch_test.step); b.default_step.dependOn(playground_step); { @@ -295,6 +532,44 @@ pub fn build(b: *std.Build) void { const ModuleTest = modules.ModuleTest; +fn discoverBuiltinRocFiles(b: *std.Build) ![]const []const u8 { + var builtin_roc_dir = try std.fs.cwd().openDir("src/build/roc", .{ .iterate = true }); + defer builtin_roc_dir.close(); + + var roc_files = std.array_list.Managed([]const u8).init(b.allocator); + errdefer roc_files.deinit(); + + 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(full_path); + } + } + + return roc_files.toOwnedSlice(); +} + +fn generateCompiledBuiltinsSource(b: *std.Build, roc_files: []const []const u8) ![]const u8 { + var builtins_source = std.array_list.Managed(u8).init(b.allocator); + errdefer builtins_source.deinit(); + const writer = builtins_source.writer(); + + 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, + }); + } + + return builtins_source.toOwnedSlice(); +} + fn add_fuzz_target( b: *std.Build, fuzz: bool, @@ -320,6 +595,10 @@ fn add_fuzz_target( .optimize = if (target.result.os.tag == .macos) .Debug else .ReleaseSafe, }), }); + // 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); @@ -344,7 +623,7 @@ fn add_fuzz_target( 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); diff --git a/build.zig.zon b/build.zig.zon index 626d8cbd24..d34225dfef 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,8 +4,8 @@ .minimum_zig_version = "0.15.1", .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 = .{ 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/base/CommonEnv.zig b/src/base/CommonEnv.zig index d1e4d8a6c8..a69d0fff15 100644 --- a/src/base/CommonEnv.zig +++ b/src/base/CommonEnv.zig @@ -96,7 +96,7 @@ pub const Serialized = struct { 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 +105,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 diff --git a/src/base/SExprTree.zig b/src/base/SExprTree.zig index 3e0bea6be3..34c44091dd 100644 --- a/src/base/SExprTree.zig +++ b/src/base/SExprTree.zig @@ -146,9 +146,9 @@ const Node = union(enum) { BytesRange: struct { begin: u32, end: u32, region: RegionInfo }, }; -children: std.ArrayList(Node), -data: std.ArrayList(u8), -stack: std.ArrayList(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 { diff --git a/src/base/Scratch.zig b/src/base/Scratch.zig index 28df091a93..d7c9e8d136 100644 --- a/src/base/Scratch.zig +++ b/src/base/Scratch.zig @@ -7,10 +7,10 @@ const DataSpan = @import("DataSpan.zig"); /// A stack for easily adding and removing index types when doing recursive operations pub fn Scratch(comptime T: type) type { return struct { - items: std.ArrayList(T), + items: std.array_list.Managed(T), const Self = @This(); - const ArrayList = std.ArrayList(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)); @@ -28,11 +28,16 @@ pub fn Scratch(comptime T: type) type { return @as(u32, @intCast(self.items.items.len)); } - /// Places a new index of type `T` in the scratch. Will panic on OOM. + /// Places a new index of type `T` in the scratch pub fn append(self: *Self, gpa: std.mem.Allocator, idx: T) std.mem.Allocator.Error!void { try self.items.append(gpa, idx); } + /// Pop an item of the scratch buffer + pub fn pop(self: *Self) ?T { + return self.items.pop(); + } + /// Creates slice from the provided indexes pub fn slice(self: *Self, start: u32, end: u32) []T { return self.items.items[@intCast(start)..@intCast(end)]; @@ -45,7 +50,7 @@ pub fn Scratch(comptime T: type) type { /// 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.ArrayList(u32)) std.mem.Allocator.Error!DataSpan { + pub fn spanFromStart(self: *Self, start: u32, gpa: std.mem.Allocator, 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)); @@ -61,7 +66,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/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig new file mode 100644 index 0000000000..58d4205bc2 --- /dev/null +++ b/src/build/builtin_compiler/main.zig @@ -0,0 +1,267 @@ +//! Build-time compiler for Roc builtin modules (Bool.roc, Result.roc, Dict.roc, and Set.roc). +//! +//! This executable runs during `zig build` on the host machine to: +//! 1. Parse and type-check the builtin .roc modules +//! 2. Serialize the resulting ModuleEnvs to binary files +//! 3. Output .bin files to zig-out/builtins/ (which get 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 ModuleEnv = can.ModuleEnv; +const Can = can.Can; +const Check = check.Check; +const Allocator = std.mem.Allocator; + +/// 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. +/// +/// Note: Command-line arguments are ignored. The .roc files are read from fixed paths. +/// The build system may pass file paths as arguments for cache tracking, but we don't use them. +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(); + + // Ignore command-line arguments - they're only used by Zig's build system for cache tracking + + // Read the .roc source files at runtime + const bool_roc_source = try std.fs.cwd().readFileAlloc(gpa, "src/build/roc/Bool.roc", 1024 * 1024); + defer gpa.free(bool_roc_source); + + const result_roc_source = try std.fs.cwd().readFileAlloc(gpa, "src/build/roc/Result.roc", 1024 * 1024); + defer gpa.free(result_roc_source); + + const dict_roc_source = try std.fs.cwd().readFileAlloc(gpa, "src/build/roc/Dict.roc", 1024 * 1024); + defer gpa.free(dict_roc_source); + + const set_roc_source = try std.fs.cwd().readFileAlloc(gpa, "src/build/roc/Set.roc", 1024 * 1024); + defer gpa.free(set_roc_source); + + // Compile Bool.roc without injecting anything (it's completely self-contained) + const bool_env = try compileModule( + gpa, + "Bool", + bool_roc_source, + &.{}, // No module dependencies + .{ .inject_bool = false, .inject_result = false }, + ); + defer { + bool_env.deinit(); + gpa.destroy(bool_env); + } + + // Verify that Bool's type declaration is at the expected index (2) + // This is critical for the compiler's hardcoded BUILTIN_BOOL constant + const bool_type_idx = bool_env.all_statements.span.start; + if (bool_type_idx != 2) { + const stderr = std.io.getStdErr().writer(); + try stderr.print("WARNING: Expected Bool at index 2, but got {}!\n", .{bool_type_idx}); + return error.UnexpectedBoolIndex; + } + + // Compile Result.roc (injects Bool since Result might use if expressions) + const result_env = try compileModule( + gpa, + "Result", + result_roc_source, + &.{}, // No module dependencies + .{ .inject_bool = true, .inject_result = false }, + ); + defer { + result_env.deinit(); + gpa.destroy(result_env); + } + + // Compile Dict.roc (needs Bool injected for if expressions, and Result for error handling) + const dict_env = try compileModule( + gpa, + "Dict", + dict_roc_source, + &.{}, // No module dependencies + .{}, // Inject Bool and Result (defaults) + ); + defer { + dict_env.deinit(); + gpa.destroy(dict_env); + } + + // Compile Set.roc (imports Dict, needs Bool and Result injected) + const set_env = try compileModule( + gpa, + "Set", + set_roc_source, + &[_]ModuleDep{ + .{ .name = "Dict", .env = dict_env }, + }, + .{}, // Inject Bool and Result (defaults) + ); + defer { + set_env.deinit(); + gpa.destroy(set_env); + } + + // Create output directory + try std.fs.cwd().makePath("zig-out/builtins"); + + // Serialize modules + try serializeModuleEnv(gpa, bool_env, "zig-out/builtins/Bool.bin"); + try serializeModuleEnv(gpa, result_env, "zig-out/builtins/Result.bin"); + try serializeModuleEnv(gpa, dict_env, "zig-out/builtins/Dict.bin"); + try serializeModuleEnv(gpa, set_env, "zig-out/builtins/Set.bin"); +} + +const ModuleDep = struct { + name: []const u8, + env: *const ModuleEnv, +}; + +fn compileModule( + gpa: Allocator, + module_name: []const u8, + source: []const u8, + deps: []const ModuleDep, + can_options: Can.InitOptions, +) !*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.common.source = source; + 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)); + const list_ident = try module_env.insertIdent(base.Ident.for_text("List")); + const box_ident = try module_env.insertIdent(base.Ident.for_text("Box")); + + const common_idents: Check.CommonIdents = .{ + .module_name = module_ident, + .list = list_ident, + .box = box_ident, + }; + + // 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()) { + std.debug.print("Parse errors in {s}:\n", .{module_name}); + for (parse_ast.tokenize_diagnostics.items) |diag| { + std.debug.print(" Tokenize error: {any}\n", .{diag}); + } + for (parse_ast.parse_diagnostics.items) |diag| { + std.debug.print(" Parse error: {any}\n", .{diag}); + } + return error.ParseError; + } + + // 4. Create module imports map (for cross-module references) + var module_envs = std.StringHashMap(*const ModuleEnv).init(gpa); + defer module_envs.deinit(); + + // Add dependencies (e.g., Dict for Set) + for (deps) |dep| { + try module_envs.put(dep.name, dep.env); + } + + // 5. Canonicalize + try module_env.initCIRFields(gpa, module_name); + + var can_result = try gpa.create(Can); + defer { + can_result.deinit(); + gpa.destroy(can_result); + } + + can_result.* = try Can.init(module_env, parse_ast, &module_envs, can_options); + + try can_result.canonicalizeFile(); + try can_result.validateForChecking(); + + // 6. Type check + // Build the list of other modules for type checking + var other_modules = std.array_list.Managed(*const ModuleEnv).init(gpa); + defer other_modules.deinit(); + + // Add dependencies + for (deps) |dep| { + try other_modules.append(dep.env); + } + + var checker = try Check.init( + gpa, + &module_env.types, + module_env, + other_modules.items, + &module_env.store.regions, + common_idents, + ); + defer checker.deinit(); + + try checker.checkFile(); + + // Check for type errors + if (checker.problems.problems.items.len > 0) { + std.debug.print("Type errors found in {s}:\n", .{module_name}); + for (checker.problems.problems.items) |prob| { + std.debug.print(" - Problem: {any}\n", .{prob}); + } + 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); +} diff --git a/src/build/modules.zig b/src/build/modules.zig index 899fd0d44f..0aa054c4f5 100644 --- a/src/build/modules.zig +++ b/src/build/modules.zig @@ -100,7 +100,7 @@ pub const ModuleType = enum { .can => &.{ .tracy, .builtins, .collections, .types, .base, .parse, .reporting }, .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 }, + .eval => &.{ .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 }, .ipc => &.{}, .repl => &.{ .base, .compile, .parse, .types, .can, .check, .builtins, .layout, .eval }, @@ -325,7 +325,7 @@ pub const RocModules = struct { const module = self.getModule(module_type); const module_filters = extendWithAggregatorFilters(b, test_filters, module_type); const test_step = b.addTest(.{ - .name = b.fmt("{s}_test", .{@tagName(module_type)}), + .name = b.fmt("{s}", .{@tagName(module_type)}), .root_module = b.createModule(.{ .root_source_file = module.root_source_file.?, .target = target, diff --git a/src/build/roc/Bool.roc b/src/build/roc/Bool.roc new file mode 100644 index 0000000000..fea74ce5fa --- /dev/null +++ b/src/build/roc/Bool.roc @@ -0,0 +1 @@ +Bool := [True, False].{} diff --git a/src/build/roc/Dict.roc b/src/build/roc/Dict.roc new file mode 100644 index 0000000000..b453b2027b --- /dev/null +++ b/src/build/roc/Dict.roc @@ -0,0 +1 @@ +Dict := [EmptyDict].{} diff --git a/src/build/roc/Result.roc b/src/build/roc/Result.roc new file mode 100644 index 0000000000..f2a788799f --- /dev/null +++ b/src/build/roc/Result.roc @@ -0,0 +1 @@ +Result(ok, err) := [Ok(ok), Err(err)].{} diff --git a/src/build/roc/Set.roc b/src/build/roc/Set.roc new file mode 100644 index 0000000000..662078af66 --- /dev/null +++ b/src/build/roc/Set.roc @@ -0,0 +1,3 @@ +import Dict + +Set := [EmptySet(Dict)].{} diff --git a/src/canonicalize/CIR.zig b/src/canonicalize/CIR.zig index 2daa535cc9..bb7605e228 100644 --- a/src/canonicalize/CIR.zig +++ b/src/canonicalize/CIR.zig @@ -274,19 +274,235 @@ pub const PatternRecordField = struct { 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) + /// TODO: Review, claude generated + 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.Num.FracRequirements { + const f64_val = self.toF64(); + return types_mod.Num.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 + /// TODO: Review, claude generated + pub fn toIntRequirements(self: IntValue) types_mod.Num.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.Num.Int.BitsNeeded.fromValue(adjusted_val); + return types_mod.Num.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 + /// TODO: Review, claude generated + /// Calculate the frac requirements of an IntValue + pub fn toFracRequirements(self: IntValue) types_mod.Num.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.Num.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, }; // RocDec type definition (for missing export) @@ -375,7 +591,8 @@ pub const Import = struct { pub const Serialized = 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 = undefined, imports: collections.SafeList(base.StringLiteral.Idx).Serialized, /// Serialize a Store into this Serialized struct, appending data to the writer @@ -387,6 +604,10 @@ pub const Import = struct { ) std.mem.Allocator.Error!void { // Serialize the imports SafeList try self.imports.serialize(&store.imports, 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 } diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 245c5b2781..8d99fa3e3c 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -59,7 +59,9 @@ const TypeVarProblem = struct { env: *ModuleEnv, parse_ir: *AST, -scopes: std.ArrayList(Scope) = .{}, +scopes: std.ArrayListUnmanaged(Scope) = .{}, +/// Special scope for rigid type variables in annotations +type_vars_scope: base.Scratch(TypeVarScope), /// Special scope for tracking exposed items from module header exposed_scope: Scope = undefined, /// Track exposed identifiers by text to handle changing indices @@ -69,7 +71,7 @@ exposed_type_texts: std.StringHashMapUnmanaged(Region) = .{}, /// Special scope for unqualified nominal tags (e.g., True, False) unqualified_nominal_tags: std.StringHashMapUnmanaged(Statement.Idx) = .{}, /// Stack of function regions for tracking var reassignment across function boundaries -function_regions: std.ArrayList(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 @@ -77,7 +79,7 @@ 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), +module_envs: ?*const std.StringHashMap(*const ModuleEnv), /// Map from module name string to Import.Idx for tracking unique imports import_indices: std.StringHashMapUnmanaged(Import.Idx), /// Scratch type variables @@ -105,6 +107,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; @@ -130,32 +133,9 @@ const RecordField = CIR.RecordField; 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); +pub const BUILTIN_BOOL: Statement.Idx = @enumFromInt(2); /// 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); +pub const BUILTIN_RESULT: Statement.Idx = @enumFromInt(13); /// Deinitialize canonicalizer resources pub fn deinit( @@ -163,6 +143,7 @@ pub fn deinit( ) void { const gpa = self.env.gpa; + self.type_vars_scope.deinit(gpa); self.exposed_scope.deinit(gpa); self.exposed_ident_texts.deinit(gpa); self.exposed_type_texts.deinit(gpa); @@ -190,7 +171,23 @@ pub fn deinit( self.scratch_free_vars.deinit(gpa); } -pub fn init(env: *ModuleEnv, parse_ir: *AST, module_envs: ?*const std.StringHashMap(*ModuleEnv)) std.mem.Allocator.Error!Self { +/// Options for initializing the canonicalizer. +/// Controls which built-in types are injected into the module's scope. +pub const InitOptions = struct { + /// Whether to inject the Bool type declaration (`Bool := [True, False]`). + /// Set to false when compiling Bool.roc itself to avoid duplication. + inject_bool: bool = true, + /// Whether to inject the Result type declaration (`Result(ok, err) := [Ok(ok), Err(err)]`). + /// Set to false when compiling Result.roc itself (if it exists). + inject_result: bool = true, +}; + +pub fn init( + env: *ModuleEnv, + parse_ir: *AST, + module_envs: ?*const std.StringHashMap(*const ModuleEnv), + options: InitOptions, +) std.mem.Allocator.Error!Self { const gpa = env.gpa; // Create the canonicalizer with scopes @@ -198,7 +195,7 @@ pub fn init(env: *ModuleEnv, parse_ir: *AST, module_envs: ?*const std.StringHash .env = env, .parse_ir = parse_ir, .scopes = .{}, - .function_regions = std.ArrayList(Region){}, + .function_regions = std.array_list.Managed(Region){}, .var_function_regions = std.AutoHashMapUnmanaged(Pattern.Idx, Region){}, .var_patterns = std.AutoHashMapUnmanaged(Pattern.Idx, void){}, .used_patterns = std.AutoHashMapUnmanaged(Pattern.Idx, void){}, @@ -210,6 +207,7 @@ 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), + .type_vars_scope = try base.Scratch(TypeVarScope).init(gpa), .exposed_scope = Scope.init(false), .scratch_tags = try base.Scratch(types.Tag).init(gpa), .unqualified_nominal_tags = std.StringHashMapUnmanaged(Statement.Idx){}, @@ -219,318 +217,189 @@ pub fn init(env: *ModuleEnv, parse_ir: *AST, module_envs: ?*const std.StringHash // 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. + const scratch_statements_start = result.env.store.scratch.?.statements.top(); + + // Inject built-in type declarations that aren't defined in this module's source + // TODO: These should ultimately come from the platform/builtin files rather than being hardcoded + if (options.inject_bool) { + const bool_idx = try result.addBuiltinTypeBool(env); + try result.env.store.addScratchStatement(bool_idx); + } + if (options.inject_result) { + const result_idx = try result.addBuiltinTypeResult(env); + try result.env.store.addScratchStatement(result_idx); + } + + result.env.builtin_statements = try result.env.store.statementSpanFrom(scratch_statements_start); + + // Debug assertion: When Bool is injected, it must be the first builtin statement + if (std.debug.runtime_safety and options.inject_bool) { + const builtin_stmts = result.env.store.sliceStatements(result.env.builtin_statements); + std.debug.assert(builtin_stmts.len >= 1); // Must have at least Bool + // Verify first builtin is Bool by checking it's a nominal_decl + const first_stmt = result.env.store.getStatement(builtin_stmts[0]); + std.debug.assert(first_stmt == .s_nominal_decl); + } // 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 }, - }; - - const type_decl_idx = try ir.addStatementAndTypeVar( - type_decl_stmt, - try ir.types.mkAlias(types.TypeIdent{ .ident_idx = type_ident }, anno_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); - - return type_decl_idx; -} - -/// 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")); - - // 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 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); - - // 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, - &.{ a_rigid, b_rigid }, - 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_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 { +/// Returns the statement index where Bool was created +fn addBuiltinTypeBool(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!Statement.Idx { const gpa = ir.gpa; const type_ident = try ir.insertIdent(base.Ident.for_text("Bool")); + const true_ident = try ir.insertIdent(base.Ident.for_text("True")); + const false_ident = try ir.insertIdent(base.Ident.for_text("False")); + + // Create a type header (lhs) => 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); + .args = .{ .span = DataSpan.empty() }, + }, .err, Region.zero()); - // 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 body (rhs) => [True, False] // - // Create the type declaration statement - const type_decl_stmt = Statement{ - .s_nominal_decl = .{ .header = header_idx, .anno = anno_idx }, - }; + const scratch_top = self.env.store.scratchTypeAnnoTop(); - 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)), - ), + const true_tag_anno_idx = try ir.addTypeAnnoAndTypeVar( + .{ .tag = .{ .name = true_ident, .args = .{ .span = DataSpan.empty() } } }, + .err, Region.zero(), ); + try self.env.store.addScratchTypeAnno(true_tag_anno_idx); - // Expose the bool type declaration idx for downstream consumers that rely on it. - BUILTIN_BOOL_TYPE = type_decl_idx; + const false_tag_anno_idx = try ir.addTypeAnnoAndTypeVar( + .{ .tag = .{ .name = false_ident, .args = .{ .span = DataSpan.empty() } } }, + .err, + Region.zero(), + ); + try self.env.store.addScratchTypeAnno(false_tag_anno_idx); - // Add to scope without any error checking (built-ins are always valid) + const tag_union_anno_idx = try ir.addTypeAnnoAndTypeVar(.{ .tag_union = .{ + .tags = try self.env.store.typeAnnoSpanFrom(scratch_top), + .ext = null, + } }, .err, Region.zero()); + + // Create the type declaration statement // + + const type_decl_idx = try ir.addStatementAndTypeVar(Statement{ + .s_nominal_decl = .{ .header = header_idx, .anno = tag_union_anno_idx }, + }, .err, Region.zero()); + + // Note: When Bool.roc is compiled without injecting builtins, Bool is at absolute index 2 (BUILTIN_BOOL). + // This is verified at build time by the builtin_compiler. + // When builtins are injected into other modules, Bool is always the FIRST builtin (builtin_statements[0]), + // though its absolute statement index may differ from BUILTIN_BOOL. + + // Introduce to scope 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); + + return type_decl_idx; +} + +/// Creates `Result(ok, err) := [Ok(ok), Err(err)]` +/// Returns the statement index where Result was created +fn addBuiltinTypeResult(self: *Self, ir: *ModuleEnv) std.mem.Allocator.Error!Statement.Idx { + const gpa = ir.gpa; + const type_ident = try ir.insertIdent(base.Ident.for_text("Result")); + const ok_tag_ident = try ir.insertIdent(base.Ident.for_text("Ok")); + const err_tag_ident = try ir.insertIdent(base.Ident.for_text("Err")); + const ok_var_ident = try ir.insertIdent(base.Ident.for_text("ok")); + const err_var_ident = try ir.insertIdent(base.Ident.for_text("err")); + + // Create a type header (lhs) => Result(ok, err) // + + const header_scratch_top = self.env.store.scratchTypeAnnoTop(); + + const ok_rigid_var = try ir.addTypeAnnoAndTypeVar(.{ .rigid_var = .{ .name = ok_var_ident } }, .err, Region.zero()); + try self.env.store.addScratchTypeAnno(ok_rigid_var); + + const err_rigid_var = try ir.addTypeAnnoAndTypeVar(.{ .rigid_var = .{ .name = err_var_ident } }, .err, Region.zero()); + try self.env.store.addScratchTypeAnno(err_rigid_var); + + const header_idx = try ir.addTypeHeaderAndTypeVar(.{ + .name = type_ident, + .args = try self.env.store.typeAnnoSpanFrom(header_scratch_top), + }, .err, Region.zero()); + + // Create the type body (rhs) => [Ok(ok), Err(err)] // + + // Create Ok(ok) + const ok_tag_scratch_top = self.env.store.scratchTypeAnnoTop(); + + const ok_rigid_var_arg = try ir.addTypeAnnoAndTypeVar(.{ .rigid_var_lookup = .{ .ref = ok_rigid_var } }, .err, Region.zero()); + try self.env.store.addScratchTypeAnno(ok_rigid_var_arg); + + const ok_tag_anno_idx = try ir.addTypeAnnoAndTypeVar( + .{ .tag = .{ + .name = ok_tag_ident, + .args = try self.env.store.typeAnnoSpanFrom(ok_tag_scratch_top), + } }, + .err, + Region.zero(), + ); + + // Create Err(err) + const err_tag_scratch_top = self.env.store.scratchTypeAnnoTop(); + + const err_rigid_var_arg = try ir.addTypeAnnoAndTypeVar(.{ .rigid_var_lookup = .{ .ref = err_rigid_var } }, .err, Region.zero()); + try self.env.store.addScratchTypeAnno(err_rigid_var_arg); + + const err_tag_anno_idx = try ir.addTypeAnnoAndTypeVar( + .{ .tag = .{ + .name = err_tag_ident, + .args = try self.env.store.typeAnnoSpanFrom(err_tag_scratch_top), + } }, + .err, + Region.zero(), + ); + + // Create tag union + const tag_scratch_top = self.env.store.scratchTypeAnnoTop(); + try self.env.store.addScratchTypeAnno(ok_tag_anno_idx); + try self.env.store.addScratchTypeAnno(err_tag_anno_idx); + + const tag_union_anno_idx = try ir.addTypeAnnoAndTypeVar(.{ .tag_union = .{ + .tags = try self.env.store.typeAnnoSpanFrom(tag_scratch_top), + .ext = null, + } }, .err, Region.zero()); + + // Create the type declaration statement // + + const type_decl_idx = try ir.addStatementAndTypeVar( + Statement{ + .s_nominal_decl = .{ .header = header_idx, .anno = tag_union_anno_idx }, + }, + .err, + Region.zero(), + ); + + // Note: When Result.roc is compiled without injecting builtins, Result ends up at index 13 (BUILTIN_RESULT) + // This is verified during build time. + // When builtins are injected into other modules (Dict, Set), Result can be at any index. + + // Add to scope + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.put(gpa, .type_decl, type_ident, type_decl_idx); + + // Add Ok and Err to unqualified_nominal_tags + try self.unqualified_nominal_tags.put(gpa, "Ok", type_decl_idx); + try self.unqualified_nominal_tags.put(gpa, "Err", type_decl_idx); + + return type_decl_idx; } // canonicalize // @@ -553,6 +422,469 @@ 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. +/// 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") +fn processTypeDeclFirstPass( + self: *Self, + type_decl: anytype, + parent_name: ?Ident.Idx, +) std.mem.Allocator.Error!void { + // Canonicalize the type declaration header first + const header_idx = try self.canonicalizeTypeHeader(type_decl.header); + const region = self.parse_ir.tokenizedRegionToRegion(type_decl.region); + + // Extract the type name from the header + const type_header = self.env.store.getTypeHeader(header_idx); + + // 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); + const qualified_name_str = try std.fmt.allocPrint( + self.env.gpa, + "{s}.{s}", + .{ parent_text, type_text }, + ); + defer self.env.gpa.free(qualified_name_str); + + const qualified_ident = base.Ident.for_text(qualified_name_str); + break :blk try self.env.insertIdent(qualified_ident); + } else type_header.name; + + // 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, + .args = type_header.args, + }; + break :blk try self.env.addTypeHeaderAndTypeVar(qualified_header, Content{ .flex = Flex.init() }, region); + } else header_idx; + + // 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 = final_header_idx, + .anno = @enumFromInt(0), // placeholder - will be replaced + }, + }, + .nominal => Statement{ + .s_nominal_decl = .{ + .header = final_header_idx, + .anno = @enumFromInt(0), // placeholder - will be replaced + }, + }, + }; + + const type_decl_stmt_idx = try self.env.addStatementAndTypeVar(placeholder_cir_type_decl, .err, region); + + // Introduce the type name into scope early to support recursive references + try self.scopeIntroduceTypeDecl(qualified_name_idx, type_decl_stmt_idx, region); + + // 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, + }, + }; + }, + .nominal => { + break :blk Statement{ + .s_nominal_decl = .{ + .header = final_header_idx, + .anno = anno_idx, + }, + }; + }, + } + }; + + // 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); + + // 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); + + // Process associated items recursively in the first pass to introduce names + // Aliases are introduced in the current scope (not a nested scope) during first pass + // They will be available when we process the associated block in the second pass + if (type_decl.associated) |assoc| { + try self.processAssociatedItemsFirstPass(qualified_name_idx, assoc.statements); + } +} + +/// 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()); + + // Look up the placeholder pattern that was created in the first pass + const pattern_idx = blk: { + const lookup_result = self.scopeLookup(.ident, qualified_ident); + switch (lookup_result) { + .found => |pattern| break :blk pattern, + .not_found => unreachable, // Pattern should have been created in first pass + } + }; + + // Canonicalize the body expression + const can_expr = try self.canonicalizeExprOrMalformed(decl.body); + + // 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.addDefAndTypeVar(def, Content{ .flex = Flex.init() }, 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, +) 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()); + + // Look up the placeholder pattern that was created in the first pass + const pattern_idx = blk: { + const lookup_result = self.scopeLookup(.ident, qualified_ident); + switch (lookup_result) { + .found => |pattern| break :blk pattern, + .not_found => unreachable, // Pattern should have been created in first pass + } + }; + + // Canonicalize the body expression + const can_expr = try self.canonicalizeExprOrMalformed(decl.body); + + // Create the annotation structure + const annotation = CIR.Annotation{ + .type_anno = type_anno_idx, + .signature = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .err, pattern_region, TypeVar), + }; + const annotation_idx = try self.env.addAnnotationAndTypeVarRedirect(annotation, ModuleEnv.varFrom(type_anno_idx), 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.addDefAndTypeVar(def, Content{ .flex = Flex.init() }, pattern_region); + return def_idx; +} + +/// Second pass helper: Canonicalize associated item definitions +fn processAssociatedItemsSecondPass( + self: *Self, + parent_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 => |type_decl| { + // Recursively process nested type declarations + 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 qualified name for nested type + const parent_text = self.env.getIdent(parent_name); + const type_text = self.env.getIdent(type_ident); + const qualified_name_str = try std.fmt.allocPrint( + self.env.gpa, + "{s}.{s}", + .{ parent_text, type_text }, + ); + defer self.env.gpa.free(qualified_name_str); + const qualified_ident = base.Ident.for_text(qualified_name_str); + const qualified_idx = try self.env.insertIdent(qualified_ident); + + try self.processAssociatedItemsSecondPass(qualified_idx, assoc.statements); + } + }, + .type_anno => |ta| { + const region = self.parse_ir.tokenizedRegionToRegion(ta.region); + const name_ident = self.parse_ir.tokens.resolveIdentifier(ta.name) orelse { + // Malformed identifier - skip this annotation + continue; + }; + + // 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; + + // If we have where clauses, create a separate s_type_anno statement + if (where_clauses != null) { + // Build qualified name for the annotation + const parent_text = self.env.getIdent(parent_name); + const name_text = self.env.getIdent(name_ident); + const qualified_name_str = try std.fmt.allocPrint( + self.env.gpa, + "{s}.{s}", + .{ parent_text, name_text }, + ); + defer self.env.gpa.free(qualified_name_str); + const qualified_ident = base.Ident.for_text(qualified_name_str); + const qualified_idx = try self.env.insertIdent(qualified_ident); + + const type_anno_stmt = Statement{ + .s_type_anno = .{ + .name = qualified_idx, + .anno = type_anno_idx, + .where = where_clauses, + }, + }; + const type_anno_stmt_idx = try self.env.addStatementAndTypeVar(type_anno_stmt, Content{ .flex = Flex.init() }, region); + try self.env.store.addScratchStatement(type_anno_stmt_idx); + } + + // Now, check the next stmt to see if it matches this anno + const next_i = i + 1; + if (next_i < stmt_idxs.len) { + const next_stmt_id = stmt_idxs[next_i]; + const next_stmt = self.parse_ir.store.getStatement(next_stmt_id); + + switch (next_stmt) { + .decl => |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_name_str = try std.fmt.allocPrint( + self.env.gpa, + "{s}.{s}", + .{ parent_text, decl_text }, + ); + defer self.env.gpa.free(qualified_name_str); + const qualified_ident = base.Ident.for_text(qualified_name_str); + const qualified_idx = try self.env.insertIdent(qualified_ident); + + // Canonicalize with the qualified name and type annotation + const def_idx = try self.canonicalizeAssociatedDeclWithAnno(decl, qualified_idx, type_anno_idx); + try self.env.store.addScratchDef(def_idx); + } + } + } + }, + else => { + // Type annotation doesn't match a declaration - continue normally + }, + } + } + }, + .decl => |decl| { + // Canonicalize the declaration with qualified 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| { + // 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_name_str = try std.fmt.allocPrint( + self.env.gpa, + "{s}.{s}", + .{ parent_text, decl_text }, + ); + defer self.env.gpa.free(qualified_name_str); + const qualified_ident = base.Ident.for_text(qualified_name_str); + const qualified_idx = try self.env.insertIdent(qualified_ident); + + // Canonicalize with the qualified name + const def_idx = try self.canonicalizeAssociatedDecl(decl, qualified_idx); + try self.env.store.addScratchDef(def_idx); + } + } else { + // Non-identifier patterns are not supported in associated blocks + const region = self.parse_ir.tokenizedRegionToRegion(decl.region); + const feature = try self.env.insertString("non-identifier patterns in associated blocks"); + try self.env.pushDiagnostic(Diagnostic{ + .not_implemented = .{ + .feature = feature, + .region = 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 + }, + } + } +} + +/// First pass helper: Process associated items and introduce them into scope with qualified names +fn processAssociatedItemsFirstPass( + self: *Self, + parent_name: Ident.Idx, + statements: AST.Statement.Span, +) std.mem.Allocator.Error!void { + for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { + const stmt = self.parse_ir.store.getStatement(stmt_idx); + switch (stmt) { + .type_decl => |type_decl| { + // Recursively process nested type declarations (this introduces the qualified name) + try self.processTypeDeclFirstPass(type_decl, parent_name); + }, + .decl => |decl| { + // Introduce declarations with qualified names for recursive references + 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 qualified name (e.g., "Foo.Bar.baz") + const parent_text = self.env.getIdent(parent_name); + const decl_text = self.env.getIdent(decl_ident); + const qualified_name_str = try std.fmt.allocPrint( + self.env.gpa, + "{s}.{s}", + .{ parent_text, decl_text }, + ); + defer self.env.gpa.free(qualified_name_str); + + const qualified_ident = base.Ident.for_text(qualified_name_str); + const qualified_idx = try self.env.insertIdent(qualified_ident); + + // Create placeholder pattern with qualified name + const region = self.parse_ir.tokenizedRegionToRegion(decl.region); + const placeholder_pattern = Pattern{ + .assign = .{ + .ident = qualified_idx, + }, + }; + const placeholder_pattern_idx = try self.env.addPatternAndTypeVar(placeholder_pattern, .err, region); + + // Introduce the qualified name to scope + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, qualified_idx, 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 = qualified_idx, + .region = 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 other statement types in first pass + }, + } + } +} + +/// 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 { @@ -566,147 +898,66 @@ pub fn canonicalizeFile( // canonicalize_header_packages(); - // First, process the header to create exposed_scope + // First, process the header to create exposed_scope and set module_kind 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), + .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); + }, + .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(); + const scratch_statements_start = self.env.store.scratch.?.statements.top(); // 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); - - // Extract the type name from the header to introduce it into scope early - const type_header = self.env.store.getTypeHeader(header_idx); - - // 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 - }, - }, - .nominal => Statement{ - .s_nominal_decl = .{ - .header = header_idx, - .anno = @enumFromInt(0), // placeholder - will be replaced - }, - }, - }; - - 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); - }; - - // Get type variables to args (lhs) - const header_arg_vars: []TypeVar = @ptrCast(self.env.store.sliceTypeAnnos(type_header.args)); - - // Get type variable to the backing type (rhs) - const anno_var = ModuleEnv.varFrom(anno_idx); - - // 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; - - // The identified of the type - const type_ident = types.TypeIdent{ .ident_idx = type_header.name }; - - // 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 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); + try self.processTypeDeclFirstPass(type_decl, null); }, else => { // Skip non-type-declaration statements in first pass @@ -714,62 +965,41 @@ pub fn canonicalizeFile( } } - // 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; + // For type modules, expose the main type and all associated items before the second pass + // This ensures unused variable checking in the third pass doesn't flag exposed items + if (self.env.module_kind == .type_module) { + const module_name_text = self.env.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; + const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; + const type_name_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; + const type_name_text = self.env.getIdent(type_name_ident); - for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { + if (std.mem.eql(u8, type_name_text, module_name_text)) { + // Expose the main type + try self.env.addExposedById(type_name_ident); + // Expose all associated items recursively + try self.exposeAssociatedItems(type_name_ident, type_decl); + break; + } + } + } + } + + // 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); - 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; - } - } - } - } - - 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); - } + _ = try self.canonicalizeStmtDecl(decl, null); }, .@"var" => |var_stmt| { // Not valid at top-level @@ -779,7 +1009,6 @@ pub fn canonicalizeFile( .stmt = string_idx, .region = region, } }); - last_type_anno = null; // Clear on non-annotation statement }, .expr => |expr_stmt| { // Not valid at top-level @@ -789,7 +1018,6 @@ pub fn canonicalizeFile( .stmt = string_idx, .region = region, } }); - last_type_anno = null; // Clear on non-annotation statement }, .crash => |crash_stmt| { // Not valid at top-level @@ -799,7 +1027,6 @@ pub fn canonicalizeFile( .stmt = string_idx, .region = region, } }); - last_type_anno = null; // Clear on non-annotation statement }, .dbg => |dbg_stmt| { // Not valid at top-level @@ -809,7 +1036,6 @@ pub fn canonicalizeFile( .stmt = string_idx, .region = region, } }); - last_type_anno = null; // Clear on non-annotation statement }, .expect => |e| { // Top-level expect statement @@ -824,9 +1050,8 @@ pub fn canonicalizeFile( const expect_stmt = Statement{ .s_expect = .{ .body = malformed, } }; - const expect_stmt_idx = try self.env.addStatementAndTypeVar(expect_stmt, Content{ .flex_var = null }, region); + const expect_stmt_idx = try self.env.addStatementAndTypeVar(expect_stmt, Content{ .flex = types.Flex.init() }, region); try self.env.store.addScratchStatement(expect_stmt_idx); - last_type_anno = null; // Clear on non-annotation statement continue; }; @@ -834,10 +1059,8 @@ pub fn canonicalizeFile( 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); + const expect_stmt_idx = try self.env.addStatementAndTypeVar(expect_stmt, Content{ .flex = types.Flex.init() }, region); try self.env.store.addScratchStatement(expect_stmt_idx); - - last_type_anno = null; // Clear on non-annotation statement }, .@"for" => |for_stmt| { // Not valid at top-level @@ -859,7 +1082,6 @@ pub fn canonicalizeFile( }, .type_decl => { // Already processed in first pass, skip - last_type_anno = null; // Clear on non-annotation statement }, .type_anno => |ta| { const region = self.parse_ir.tokenizedRegionToRegion(ta.region); @@ -874,6 +1096,7 @@ pub fn canonicalizeFile( .region = region, }, }); + continue; }; @@ -883,9 +1106,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); @@ -912,22 +1136,112 @@ pub fn canonicalizeFile( .where = where_clauses, }, }; - const type_anno_stmt_idx = try self.env.addStatementAndTypeVar(type_anno_stmt, Content{ .flex_var = null }, region); + const type_anno_stmt_idx = try self.env.addStatementAndTypeVar(type_anno_stmt, Content{ .flex = types.Flex.init() }, region); try self.env.store.addScratchStatement(type_anno_stmt_idx); } - // 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, - }; + // Now, check the next stmt to see if it matches this anno + const next_i = i + 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); + + switch (next_stmt) { + .decl => |decl| { + i = next_i; + _ = try self.canonicalizeStmtDecl(decl, TypeAnnoIdent{ + .name = name_ident, + .anno_idx = type_anno_idx, + }); + }, + else => { + // TODO: Issue diagnostic? + }, + } + } }, .malformed => |malformed| { // We won't touch this since it's already a parse error. _ = malformed; - last_type_anno = null; // Clear on non-annotation statement + }, + } + } + + // Third pass: Process associated items in type declarations + 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) |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; + + // Enter a new scope for the associated block + try self.scopeEnter(self.env.gpa, false); // false = not a function boundary + defer self.scopeExit(self.env.gpa) catch unreachable; + + // Re-introduce the aliases from first pass + // (We need to rebuild them since we're in a new scope) + 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 qualified name + const parent_text = self.env.getIdent(type_ident); + const type_text = self.env.getIdent(unqualified_ident); + const qualified_name_str = try std.fmt.allocPrint( + self.env.gpa, + "{s}.{s}", + .{ parent_text, type_text }, + ); + defer self.env.gpa.free(qualified_name_str); + const qualified_ident_idx = try self.env.insertIdent(base.Ident.for_text(qualified_name_str)); + + // Look up and alias + if (self.scopeLookupTypeDecl(qualified_ident_idx)) |qualified_type_decl_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.introduceTypeAlias(self.env.gpa, unqualified_ident, qualified_type_decl_idx); + } + }, + .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 qualified name + const parent_text = self.env.getIdent(type_ident); + const decl_text = self.env.getIdent(decl_ident); + const qualified_name_str = try std.fmt.allocPrint( + self.env.gpa, + "{s}.{s}", + .{ parent_text, decl_text }, + ); + defer self.env.gpa.free(qualified_name_str); + const qualified_ident_idx = try self.env.insertIdent(base.Ident.for_text(qualified_name_str)); + + // Look up the qualified pattern + switch (self.scopeLookup(.ident, qualified_ident_idx)) { + .found => |pattern_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); + }, + .not_found => {}, + } + } + } + }, + else => {}, + } + } + + try self.processAssociatedItemsSecondPass(type_ident, assoc.statements); + } + }, + else => { + // Skip non-type-declaration statements in third pass }, } } @@ -944,11 +1258,111 @@ pub fn canonicalizeFile( // Assert that everything is in-sync self.env.debugAssertArraysInSync(); - - // Freeze the interners after canonicalization is complete - self.env.freezeInterners(); } +/// 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 + }, + } +} + +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, pattern_region); + } + } else { + // TODO: Diagnostic + } + } + } + + // 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]; + + // 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); + } +} + +/// 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, +}; + fn collectBoundVars(self: *Self, pattern_idx: Pattern.Idx, bound_vars: *std.AutoHashMapUnmanaged(Pattern.Idx, void)) !void { const pattern = self.env.store.getPattern(pattern_idx); switch (pattern) { @@ -988,7 +1402,7 @@ fn collectBoundVars(self: *Self, pattern_idx: Pattern.Idx, bound_vars: *std.Auto } } }, - .int_literal, + .num_literal, .small_dec_literal, .dec_literal, .frac_f32_literal, @@ -1359,10 +1773,12 @@ fn canonicalizeImportStatement( // 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); + // 5. 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); + try self.introduceExposedItemsIntoScope(cir_exposes, module_name, alias, 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); @@ -1377,7 +1793,7 @@ fn canonicalizeImportStatement( }, }; - const import_idx = try self.env.addStatementAndTypeVar(cir_import, Content{ .flex_var = null }, self.parse_ir.tokenizedRegionToRegion(import_stmt.region)); + const import_idx = try self.env.addStatementAndTypeVar(cir_import, Content{ .flex = types.Flex.init() }, 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 @@ -1458,12 +1874,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); @@ -1511,11 +1926,9 @@ 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.addExposedItemAndTypeVar(cir_exposed, .{ .flex = types.Flex.init() }, 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 @@ -1523,6 +1936,7 @@ fn introduceExposedItemsIntoScope( self: *Self, exposed_items_span: CIR.ExposedItem.Span, module_name: Ident.Idx, + module_alias: Ident.Idx, import_region: Region, ) std.mem.Allocator.Error!void { const exposed_items_slice = self.env.store.sliceExposedItems(exposed_items_span); @@ -1541,6 +1955,20 @@ fn introduceExposedItemsIntoScope( // Get the module's exposed_items const module_env = envs_map.get(module_name_text).?; + // For type modules, auto-introduce the main type with the alias name + 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); + } + }, + else => {}, + } + // Validate each exposed item for (exposed_items_slice) |exposed_item_idx| { const exposed_item = self.env.store.getExposedItem(exposed_item_idx); @@ -1598,16 +2026,16 @@ fn introduceExposedItemsIntoScope( } } +/// 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()); const pattern_idx = blk: { if (try self.canonicalizePattern(decl.pattern)) |idx| { @@ -1620,17 +2048,7 @@ fn canonicalizeDeclWithAnnotation( } }; - 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 // @@ -1649,10 +2067,10 @@ fn canonicalizeDeclWithAnnotation( const region = self.parse_ir.tokenizedRegionToRegion(decl.region); const def_idx = self.env.addDefAndTypeVar(.{ .pattern = pattern_idx, - .expr = expr_idx, - .annotation = annotation, + .expr = can_expr.idx, + .annotation = mb_anno_idx, .kind = .let, - }, Content{ .flex_var = null }, region); + }, Content{ .flex = types.Flex.init() }, region); return def_idx; } @@ -1752,27 +2170,23 @@ fn canonicalizeSingleQuote( const token_text = self.parse_ir.resolve(token); 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) { + if (comptime Idx == Expr.Idx) { const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ - .e_int = .{ + .e_num = .{ .value = value_content, + .kind = .int_unbound, }, - }, type_content, region); + }, .err, 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); + } else if (comptime Idx == Pattern.Idx) { + const pat_idx = try self.env.addPatternAndTypeVar(Pattern{ .num_literal = .{ + .value = value_content, + .kind = .int_unbound, + } }, .err, region); return pat_idx; } else { @compileError("Unsupported Idx type"); @@ -1821,7 +2235,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.addRecordFieldAndTypeVar(cir_field, Content{ .flex = types.Flex.init() }, self.parse_ir.tokenizedRegionToRegion(field.region)); } /// Parse an integer with underscores. @@ -1866,16 +2280,14 @@ pub fn canonicalizeExpr( // 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| { @@ -1888,10 +2300,11 @@ pub fn canonicalizeExpr( const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ .e_call = .{ + .func = can_fn_expr.idx, .args = args_span, .called_via = CalledVia.apply, }, - }, Content{ .flex_var = null }, region); + }, .err, 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 }; @@ -1902,6 +2315,36 @@ pub fn canonicalizeExpr( // Check if this is a module-qualified identifier const qualifier_tokens = self.parse_ir.store.tokenSlice(e.qualifiers); if (qualifier_tokens.len > 0) { + // 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)); + + // Try local lookup first + 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.addExprAndTypeVar(CIR.Expr{ .e_lookup_local = .{ + .pattern_idx = found_pattern_idx, + } }, .err, region); + + const free_vars_start = self.scratch_free_vars.top(); + try self.scratch_free_vars.append(self.env.gpa, found_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 }; + }, + .not_found => { + // Not a local qualified identifier, try module-qualified 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 @@ -1941,7 +2384,7 @@ pub fn canonicalizeExpr( .module_idx = import_idx, .target_node_idx = target_node_idx, .region = region, - } }, Content{ .flex_var = null }, region); + } }, Content{ .flex = types.Flex.init() }, region); return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null, @@ -1952,20 +2395,21 @@ pub fn canonicalizeExpr( // 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); + // TODO(RANK) + const expr_idx = try self.env.addExprAndTypeVar(CIR.Expr{ .e_lookup_local = .{ + .pattern_idx = found_pattern_idx, + } }, .err, region); const free_vars_start = self.scratch_free_vars.top(); - try self.scratch_free_vars.append(self.env.gpa, pattern_idx); + try self.scratch_free_vars.append(self.env.gpa, found_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 }; }, @@ -2005,7 +2449,7 @@ pub fn canonicalizeExpr( .module_idx = import_idx, .target_node_idx = target_node_idx, .region = region, - } }, Content{ .flex_var = null }, region); + } }, .err, region); return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; } @@ -2082,109 +2526,111 @@ pub fn canonicalizeExpr( return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; } - // 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 { + // TODO: Create a new error type + const expr_idx = try self.env.pushMalformed(Expr.Idx, Diagnostic{ .invalid_num_literal = .{ .region = region } }); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; } }; - 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.addExprAndTypeVar( + .{ .e_num = .{ .value = int_value, .kind = num_suffix } }, + .err, + region, + ); + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; } - 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), + // Insert concrete expr + const expr_idx = blk: { + const is_not_base10 = int_base != DEFAULT_BASE; + if (is_not_base10) { + // For non-decimal integers (hex, binary, octal), set as an int + break :blk try self.env.addExprAndTypeVar( + CIR.Expr{ .e_num = .{ + .value = int_value, + .kind = .int_unbound, + } }, + .err, + region, + ); + } else { + // For decimal (base 10), use a num so it can be either Int or Frac + break :blk try self.env.addExprAndTypeVar( + CIR.Expr{ .e_num = .{ + .value = int_value, + .kind = .num_unbound, + } }, + .err, + region, + ); + } }; - 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, - region, - ); return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; }, .frac => |e| { @@ -2201,25 +2647,31 @@ pub fn canonicalizeExpr( }; 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 }; } const expr_idx = try self.env.addExprAndTypeVar( - .{ .e_frac_f32 = .{ .value = @floatCast(f64_val) } }, - .{ .structure = FlatType{ .num = .{ .frac_precision = .f32 } } }, + .{ .e_frac_f32 = .{ + .value = @floatCast(f64_val), + .has_suffix = true, + } }, + .err, region, ); return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; } 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 } } }, + .{ .e_frac_f64 = .{ + .value = f64_val, + .has_suffix = true, + } }, + .err, region, ); return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; } 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 }; } @@ -2228,8 +2680,11 @@ pub fn canonicalizeExpr( return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; }; const expr_idx = try self.env.addExprAndTypeVar( - .{ .e_frac_dec = .{ .value = dec_val } }, - .{ .structure = FlatType{ .num = .{ .frac_precision = .dec } } }, + .{ .e_dec = .{ + .value = dec_val, + .has_suffix = true, + } }, + .err, region, ); return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; @@ -2248,38 +2703,31 @@ pub fn canonicalizeExpr( }, }; - // 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.addExprAndTypeVar(cir_expr, .err, region); return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; }, @@ -2332,16 +2780,9 @@ pub fn canonicalizeExpr( return CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; } - // 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); + .e_list = .{ .elems = elems_span }, + }, .err, 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 }; @@ -2409,7 +2850,7 @@ pub fn canonicalizeExpr( // 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()), + self.env.store.scratch.?.exprs.slice(scratch_top, self.env.store.scratchExprTop()), )), ); @@ -2451,7 +2892,7 @@ pub fn canonicalizeExpr( } // 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(); @@ -2492,7 +2933,7 @@ pub fn canonicalizeExpr( // 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(self.env.gpa, can_field_idx); } } else { // TODO: Add diagnostic on duplicate record field @@ -2500,7 +2941,7 @@ pub fn canonicalizeExpr( } 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(self.env.gpa, can_field_idx); } } } @@ -2556,17 +2997,17 @@ pub fn canonicalizeExpr( // args const gpa = self.env.gpa; - const args_start = self.env.store.scratch_patterns.top(); + 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(gpa, 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(gpa, malformed_idx); } } const args_span = try self.env.store.patternSpanFrom(args_start); @@ -2625,7 +3066,7 @@ pub fn canonicalizeExpr( } const capture_info: Expr.Capture.Span = blk: { - const scratch_start = self.env.store.scratch_captures.top(); + 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.*; @@ -2639,7 +3080,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.addCaptureAndTypeVar(capture, types.Content{ .flex = types.Flex.init() }, region); try self.env.store.addScratchCapture(capture_idx); } @@ -2737,7 +3178,7 @@ pub fn canonicalizeExpr( const expr_idx = try self.env.addExprAndTypeVar(Expr{ .e_binop = Expr.Binop.init(op, can_lhs.idx, can_rhs.idx), - }, Content{ .flex_var = null }, region); + }, Content{ .flex = types.Flex.init() }, 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 }; @@ -2762,7 +3203,7 @@ pub fn canonicalizeExpr( // Create unary minus CIR expression const expr_idx = try self.env.addExprAndTypeVar(Expr{ .e_unary_minus = Expr.UnaryMinus.init(can_operand.idx), - }, Content{ .flex_var = null }, region); + }, Content{ .flex = types.Flex.init() }, region); return CanonicalizedExpr{ .idx = expr_idx, .free_vars = can_operand.free_vars }; }, @@ -2773,7 +3214,7 @@ pub fn canonicalizeExpr( // Create unary not CIR expression const expr_idx = try self.env.addExprAndTypeVar(Expr{ .e_unary_not = Expr.UnaryNot.init(can_operand.idx), - }, Content{ .flex_var = null }, region); + }, Content{ .flex = types.Flex.init() }, region); return CanonicalizedExpr{ .idx = expr_idx, .free_vars = can_operand.free_vars }; }, @@ -2825,7 +3266,7 @@ pub fn canonicalizeExpr( .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.addIfBranchAndTypeVar(if_branch, Content{ .flex = types.Flex.init() }, 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 @@ -2858,7 +3299,7 @@ pub fn canonicalizeExpr( .branches = branches_span, .final_else = final_else, }, - }, Content{ .flex_var = null }, region); + }, Content{ .flex = types.Flex.init() }, region); // Immediately redirect the if expression's type variable to the first branch's body const first_branch = self.env.store.getIfBranch(branches[0]); @@ -2919,7 +3360,7 @@ pub fn canonicalizeExpr( const branch_pattern_idx = try self.env.addMatchBranchPatternAndTypeVar(Expr.Match.BranchPattern{ .pattern = pattern_idx, .degenerate = false, - }, Content{ .flex_var = null }, alt_pattern_region); + }, Content{ .flex = types.Flex.init() }, alt_pattern_region); try self.env.store.addScratchMatchBranchPattern(branch_pattern_idx); } }, @@ -2939,7 +3380,7 @@ pub fn canonicalizeExpr( const branch_pattern_idx = try self.env.addMatchBranchPatternAndTypeVar(Expr.Match.BranchPattern{ .pattern = pattern_idx, .degenerate = false, - }, Content{ .flex_var = null }, pattern_region); + }, Content{ .flex = types.Flex.init() }, pattern_region); try self.env.store.addScratchMatchBranchPattern(branch_pattern_idx); }, } @@ -2970,7 +3411,7 @@ pub fn canonicalizeExpr( .guard = null, .redundant = @enumFromInt(0), // TODO }, - Content{ .flex_var = null }, + Content{ .flex = types.Flex.init() }, body_region, ); @@ -2993,7 +3434,7 @@ pub fn canonicalizeExpr( }; // Create initial content for the match expression - const initial_content = if (mb_branch_var) |_| Content{ .flex_var = null } else Content{ .err = {} }; + const initial_content = if (mb_branch_var) |_| Content{ .flex = types.Flex.init() } 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 @@ -3014,7 +3455,7 @@ pub fn canonicalizeExpr( // Create debug expression const dbg_expr = try self.env.addExprAndTypeVar(Expr{ .e_dbg = .{ .expr = can_inner.idx, - } }, Content{ .flex_var = null }, region); + } }, Content{ .flex = types.Flex.init() }, region); return CanonicalizedExpr{ .idx = dbg_expr, .free_vars = can_inner.free_vars }; }, @@ -3028,171 +3469,11 @@ pub fn canonicalizeExpr( }, .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); + const ellipsis_expr = try self.env.addExprAndTypeVar(Expr{ .e_ellipsis = .{} }, Content{ .flex = types.Flex.init() }, region); return CanonicalizedExpr{ .idx = ellipsis_expr, .free_vars = null }; }, .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| { // We won't touch this since it's already a parse error. @@ -3246,7 +3527,7 @@ 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 ext_var = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .{ .flex = types.Flex.init() }, 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); @@ -3332,39 +3613,95 @@ fn canonicalizeTagExpr(self: *Self, e: AST.TagExpr, mb_args: ?AST.Expr.Span, reg }, } } 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 = 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); + + 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 = full_type_ident, + .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, + }; + }, + } + } + + // 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_name = self.scopeLookupModule(first_tok_ident).?; // Already checked above 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, @@ -3372,36 +3709,54 @@ fn canonicalizeTagExpr(self: *Self, e: AST.TagExpr, mb_args: ?AST.Expr.Span, reg } }), .free_vars = null }; }; - // 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 }; + break :blk 0; }; - const target_ident = module_env.common.findIdent(type_tok_text) orelse { - // Type is not exposed by the module + const target_ident = module_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 }; }; const other_module_node_id = module_env.getExposedNodeIndexById(target_ident) orelse { - // Type is not exposed by the module + // 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 }; }; // 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{ @@ -3411,7 +3766,7 @@ fn canonicalizeTagExpr(self: *Self, e: AST.TagExpr, mb_args: ?AST.Expr.Span, reg .backing_expr = tag_expr_idx, .backing_type = .tag, }, - }, type_content, region); + }, .err, region); const free_vars_slice = self.scratch_free_vars.slice(free_vars_start, self.scratch_free_vars.top()); return CanonicalizedExpr{ @@ -3509,7 +3864,6 @@ fn canonicalizePattern( 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); @@ -3517,7 +3871,7 @@ fn canonicalizePattern( // Create a Pattern node for our identifier const pattern_idx = try self.env.addPatternAndTypeVar(Pattern{ .assign = .{ .ident = ident_idx, - } }, .{ .flex_var = null }, region); + } }, .err, 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)) { @@ -3562,7 +3916,7 @@ fn canonicalizePattern( .underscore = {}, }; - const pattern_idx = try self.env.addPatternAndTypeVar(underscore_pattern, Content{ .flex_var = null }, region); + const pattern_idx = try self.env.addPatternAndTypeVar(underscore_pattern, .err, region); return pattern_idx; }, @@ -3636,87 +3990,76 @@ 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; + // TODO: Create a new error 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.addPatternAndTypeVar( + .{ .num_literal = .{ + .value = .{ .bytes = @bitCast(i128_val), .kind = .i128 }, + .kind = num_suffix, + } }, + .err, + 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, + Pattern{ .num_literal = .{ + .value = CIR.IntValue{ .bytes = @bitCast(i128_val), .kind = .i128 }, + .kind = .num_unbound, + } }, + .err, region, ); return pattern_idx; @@ -3735,25 +4078,25 @@ 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( .{ .frac_f32_literal = .{ .value = @floatCast(f64_val) } }, - .{ .structure = FlatType{ .num = .{ .frac_precision = .f32 } } }, + .err, region, ); return pattern_idx; } else if (std.mem.eql(u8, suffix, "f64")) { const pattern_idx = try self.env.addPatternAndTypeVar( .{ .frac_f64_literal = .{ .value = f64_val } }, - .{ .structure = FlatType{ .num = .{ .frac_precision = .f64 } } }, + .err, 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; } @@ -3762,8 +4105,8 @@ fn canonicalizePattern( return malformed_idx; }; const pattern_idx = try self.env.addPatternAndTypeVar( - .{ .dec_literal = .{ .value = dec_val } }, - .{ .structure = FlatType{ .num = .{ .frac_precision = .dec } } }, + .{ .dec_literal = .{ .value = dec_val, .has_suffix = true } }, + .err, region, ); return pattern_idx; @@ -3779,18 +4122,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 = .{ @@ -3802,21 +4133,23 @@ 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.addPatternAndTypeVar(cir_pattern, .err, region); return pattern_idx; }, @@ -3835,7 +4168,7 @@ fn canonicalizePattern( .literal = literal, }, }; - const pattern_idx = try self.env.addPatternAndTypeVar(str_pattern, Content{ .structure = .str }, region); + const pattern_idx = try self.env.addPatternAndTypeVar(str_pattern, .err, region); return pattern_idx; }, @@ -3843,23 +4176,24 @@ fn canonicalizePattern( return try self.canonicalizeSingleQuote(e.region, e.token, Pattern.Idx); }, .tag => |e| { + const gpa = self.env.gpa; 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(gpa, 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(gpa, malformed_idx); } } const args = try self.env.store.patternSpanFrom(patterns_start); @@ -3867,9 +4201,9 @@ fn canonicalizePattern( // 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 ext_var = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .{ .flex = types.Flex.init() }, 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); + _ = 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{ @@ -3877,7 +4211,7 @@ fn canonicalizePattern( .name = tag_name, .args = args, }, - }, tag_union_type, region); + }, .err, 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) @@ -3982,7 +4316,7 @@ 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 }; }; @@ -4010,7 +4344,7 @@ 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{ @@ -4020,7 +4354,7 @@ fn canonicalizePattern( .backing_pattern = tag_pattern_idx, .backing_type = .tag, }, - }, type_content, region); + }, .err, region); return nominal_pattern_idx; } @@ -4041,12 +4375,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 @@ -4056,12 +4392,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.addRecordDestructAndTypeVar(record_destruct, .err, 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.addPatternAndTypeVar(assign_pattern, .err, field_region); const record_destruct = CIR.Pattern.RecordDestruct{ .label = field_name_ident, @@ -4069,7 +4405,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.addRecordDestructAndTypeVar(record_destruct, .err, field_region); try self.env.store.addScratchRecordDestruct(destruct_idx); // Introduce the identifier into scope @@ -4114,19 +4450,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{ .record_destructure = .{ - .whole_var = whole_var, - .ext_var = ext_var, .destructs = destructs_span, }, - }, .{ .flex_var = null }, region); + }, .err, region); return pattern_idx; }, @@ -4149,24 +4478,17 @@ 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{ .tuple = .{ .patterns = patterns_span, }, - }, Content{ .structure = FlatType{ - .tuple = types.Tuple{ .elems = elems_var_range }, - } }, region); + }, .err, region); return pattern_idx; }, .list => |e| { const region = self.parse_ir.tokenizedRegionToRegion(e.region); + const gpa = self.env.gpa; // Mark the start of scratch patterns for non-rest patterns only const scratch_top = self.env.store.scratchPatternTop(); @@ -4199,11 +4521,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 = .{ .ident = ident_idx, - } }, Content{ .flex_var = null }, name_region); + } }, .err, name_region); // Introduce the identifier into scope switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident_idx, assign_idx, false, true)) { @@ -4239,19 +4559,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(gpa, 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(gpa, malformed_idx); } } } @@ -4261,50 +4581,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 + // Empty list pattern const pattern_idx = try self.env.addPatternAndTypeVar(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); + }, .err, 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{ .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); + }, .err, region); return pattern_idx; }, @@ -4350,7 +4645,7 @@ fn canonicalizePattern( }, }; - const pattern_idx = try self.env.addPatternAndTypeVar(as_pattern, .{ .flex_var = null }, region); + const pattern_idx = try self.env.addPatternAndTypeVar(as_pattern, .err, region); // Introduce the identifier into scope switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident_idx, pattern_idx, false, true)) { @@ -4440,25 +4735,6 @@ 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 { @@ -4563,7 +4839,7 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { .numerator = small.numerator, .denominator_power_of_ten = small.denominator_power_of_ten, .requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = fitsInF32(small_f64_val), + .fits_in_f32 = CIR.fitsInF32(small_f64_val), .fits_in_dec = true, }, }, @@ -4580,7 +4856,7 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { .numerator = @as(i16, @intFromFloat(rounded)), .denominator_power_of_ten = 0, .requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = fitsInF32(f64_val), + .fits_in_f32 = CIR.fitsInF32(f64_val), .fits_in_dec = true, }, }, @@ -4590,7 +4866,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; @@ -4612,7 +4888,7 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { .f64 = .{ .value = f64_val, .requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = fitsInF32(f64_val), + .fits_in_f32 = CIR.fitsInF32(f64_val), .fits_in_dec = false, }, }, @@ -4630,7 +4906,7 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { .f64 = .{ .value = f64_val, .requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = fitsInF32(f64_val), + .fits_in_f32 = CIR.fitsInF32(f64_val), .fits_in_dec = false, }, }, @@ -4641,7 +4917,7 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { .dec = .{ .value = RocDec{ .num = dec_num }, .requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = fitsInF32(f64_val), + .fits_in_f32 = CIR.fitsInF32(f64_val), .fits_in_dec = true, }, }, @@ -4654,7 +4930,7 @@ fn parseFracLiteral(token_text: []const u8) !FracLiteralResult { .f64 = .{ .value = f64_val, .requirements = types.Num.Frac.Requirements{ - .fits_in_f32 = fitsInF32(f64_val), + .fits_in_f32 = CIR.fitsInF32(f64_val), .fits_in_dec = false, }, }, @@ -4914,14 +5190,13 @@ 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); - return try self.env.addTypeAnnoAndTypeVarRedirect(.{ .ty_var = .{ - .name = name_ident, + return try self.env.addTypeAnnoAndTypeVarRedirect(.{ .rigid_var_lookup = .{ + .ref = found_anno_idx, } }, ModuleEnv.varFrom(found_anno_idx), region); }, .not_found => { @@ -4932,8 +5207,8 @@ fn canonicalizeTypeAnnoHelp(self: *Self, anno_idx: AST.TypeAnno.Idx, type_anno_c // Track this type variable for underscore validation try self.scratch_type_var_validation.append(self.env.gpa, name_ident); - const content = types.Content{ .rigid_var = name_ident }; - const new_anno_idx = try self.env.addTypeAnnoAndTypeVar(.{ .ty_var = .{ + const content = types.Content{ .rigid = types.Rigid.init(name_ident) }; + const new_anno_idx = try self.env.addTypeAnnoAndTypeVar(.{ .rigid_var = .{ .name = name_ident, } }, content, region); @@ -4973,14 +5248,13 @@ 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); - return try self.env.addTypeAnnoAndTypeVarRedirect(.{ .ty_var = .{ - .name = name_ident, + return try self.env.addTypeAnnoAndTypeVarRedirect(.{ .rigid_var_lookup = .{ + .ref = found_anno_idx, } }, ModuleEnv.varFrom(found_anno_idx), region); }, .not_found => { @@ -4991,8 +5265,8 @@ fn canonicalizeTypeAnnoHelp(self: *Self, anno_idx: AST.TypeAnno.Idx, type_anno_c // Track this type variable for underscore validation try self.scratch_type_var_validation.append(self.env.gpa, name_ident); - const content = types.Content{ .rigid_var = name_ident }; - const new_anno_idx = try self.env.addTypeAnnoAndTypeVar(.{ .ty_var = .{ + const content = types.Content{ .rigid = types.Rigid.init(name_ident) }; + const new_anno_idx = try self.env.addTypeAnnoAndTypeVar(.{ .rigid_var = .{ .name = name_ident, } }, content, region); @@ -5013,7 +5287,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; @@ -5033,7 +5307,7 @@ fn canonicalizeTypeAnnoHelp(self: *Self, anno_idx: AST.TypeAnno.Idx, type_anno_c if (type_anno_ctx.isTypeDeclAndHasUnderscore()) { break :blk types.Content{ .err = {} }; } else { - break :blk types.Content{ .flex_var = null }; + break :blk types.Content{ .flex = types.Flex.init() }; } }; @@ -5078,16 +5352,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(); @@ -5101,52 +5370,54 @@ fn canonicalizeTypeAnnoBasicType( const type_name_region = self.parse_ir.tokens.resolve(ty.token); if (qualifier_toks.len == 0) { - // Unqualified type - - // TODO: Check for List, Box, and Str here (since they are primitives) - - const type_decl_idx = self.scopeLookupTypeDecl(type_name_ident) orelse { - // Type not found in scope - issue diagnostic - try self.env.pushDiagnostic(Diagnostic{ .undeclared_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.addTypeAnnoAndTypeVar(CIR.TypeAnno{ .lookup = .{ .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; - }, + .base = .{ .builtin = builtin_type }, + } }, .err, region); + } else { + // If it's not a builtin, look up in scope + const type_decl_idx = self.scopeLookupTypeDecl(type_name_ident) orelse { + return try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ .undeclared_type = .{ + .name = type_name_ident, + .region = type_name_region, + } }); + }; + return try self.env.addTypeAnnoAndTypeVar(CIR.TypeAnno{ .lookup = .{ + .name = type_name_ident, + .base = .{ .local = .{ .decl_idx = type_decl_idx } }, + } }, .err, region); } } 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.addTypeAnnoAndTypeVar(CIR.TypeAnno{ .lookup = .{ + .name = qualified_name_ident, + .base = .{ .local = .{ .decl_idx = type_decl_idx } }, + } }, .err, 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, ); @@ -5155,64 +5426,61 @@ fn canonicalizeTypeAnnoBasicType( // 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 = .{ + 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_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 }; + break :blk 0; }; const target_ident = module_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 { // 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.addTypeAnnoAndTypeVar(CIR.TypeAnno{ .lookup = .{ .name = type_name_ident, .base = .{ .external = .{ + .module_idx = import_idx, + .target_node_idx = target_node_idx, + } } } }, .err, region); } } @@ -5235,7 +5503,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); @@ -5245,7 +5513,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(); @@ -5261,45 +5529,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.addTypeAnnoAndTypeVar(.{ .apply = .{ + .name = ty.name, + .base = ty.base, .args = args_span, - } }, ModuleEnv.varFrom(local_decl_idx), region); + } }, .err, 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, } } @@ -5476,12 +5720,9 @@ fn canonicalizeTypeAnnoTagUnion( 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); + .tag => |tag| { + const args_slice: []TypeVar = @ptrCast(self.env.store.sliceTypeAnnos(tag.args)); + break :blk try self.env.types.mkTag(tag.name, args_slice); }, .malformed => { continue; @@ -5557,9 +5798,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.addTypeAnnoAndTypeVar(.{ .tag = .{ + .name = ident_idx, + .args = .{ .span = DataSpan.empty() }, + } }, .err, region); }, .apply => |apply| { // For tags with arguments like `Some(Str)`, validate the arguments but not the tag name @@ -5591,10 +5833,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.addTypeAnnoAndTypeVar(.{ .tag = .{ + .name = type_name, .args = args, - } }, Content{ .flex_var = null }, region); + } }, Content{ .flex = types.Flex.init() }, region); }, else => { return try self.env.pushMalformed(TypeAnno.Idx, Diagnostic{ @@ -5661,7 +5903,7 @@ fn canonicalizeTypeHeader(self: *Self, header_idx: AST.TypeHeader.Idx) std.mem.A 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); + }, Content{ .flex = types.Flex.init() }, node_region); } const ast_header = self.parse_ir.store.getTypeHeader(header_idx) catch unreachable; // Malformed handled above @@ -5673,9 +5915,18 @@ fn canonicalizeTypeHeader(self: *Self, header_idx: AST.TypeHeader.Idx) std.mem.A 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); + }, Content{ .flex = types.Flex.init() }, region); }; + // Check if this is a builtin type + // TODO: Can we compare idents or something here? The byte slice comparison is ineffecient + if (TypeAnno.Builtin.fromBytes(self.env.getIdentText(name_ident))) |_| { + 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); @@ -5704,9 +5955,9 @@ fn canonicalizeTypeHeader(self: *Self, header_idx: AST.TypeHeader.Idx) std.mem.A } }); } - const param_anno = try self.env.addTypeAnnoAndTypeVar(.{ .ty_var = .{ + const param_anno = try self.env.addTypeAnnoAndTypeVar(.{ .rigid_var = .{ .name = param_ident, - } }, Content{ .rigid_var = param_ident }, param_region); + } }, Content{ .rigid = types.Rigid.init(param_ident) }, param_region); try self.env.store.addScratchTypeAnno(param_anno); }, .underscore => |underscore_param| { @@ -5740,12 +5991,10 @@ 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); }, } } @@ -5755,162 +6004,245 @@ fn canonicalizeTypeHeader(self: *Self, header_idx: AST.TypeHeader.Idx) std.mem.A return try self.env.addTypeHeaderAndTypeVar(.{ .name = name_ident, .args = args, - }, Content{ .flex_var = null }, region); + }, Content{ .flex = types.Flex.init() }, 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: @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, -}; + // Keep track of the start position for statements + const stmt_start = self.env.store.scratch.?.statements.top(); -/// 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(); - - // 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. + // TODO Use a temporary scratch space for the block's free variables // - // We can't have the `defer` outide the switch branches because not all - // branches should reset `last_type_anno` + // 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); - 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); + var captures = std.AutoHashMapUnmanaged(Pattern.Idx, void){}; + defer captures.deinit(self.env.gpa); - // 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); + // 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); + }, + .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); - 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 }, - }; + // 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 = types.Flex.init() }, 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 = types.Flex.init() }, crash_region); }, - .not_found => { - // Not found in scope, fall through to regular declaration + 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 = null }; }, - else => {}, + 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); + const mb_canonicailzed_stmt = stmt_result.canonicalized_stmt; + + // If we have a second statement (e.g., type annotation), process it too + if (stmt_result.second_canonicalized_stmt) |other_stmt| { + try self.env.store.addScratchStatement(other_stmt.idx); + + // Collect bound variables for the other statement + const cir_other_stmt = self.env.store.getStatement(other_stmt.idx); + switch (cir_other_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 other statement + if (other_stmt.free_vars) |fvs| { + for (fvs) |fv| { + if (!bound_vars.contains(fv)) { + try captures.put(self.env.gpa, fv, {}); + } + } + } + + // Skip the next statement since we processed it + i += 1; } - // check against last anno + // Post processing for the stmt + if (mb_canonicailzed_stmt) |canonicailzed_stmt| { + try self.env.store.addScratchStatement(canonicailzed_stmt.idx); - // 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()), - } }); - }; + // Collect bound variables for the + const cir_stmt = self.env.store.getStatement(canonicailzed_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 => {}, + } - 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; + // Collect free vars from the statement into the block's scratch space + if (canonicailzed_stmt.free_vars) |fvs| { + for (fvs) |fv| { + if (!bound_vars.contains(fv)) { + try captures.put(self.env.gpa, 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); + // 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 }, block_region); + break :blk CanonicalizedExpr{ .idx = expr_idx, .free_vars = null }; + }; + const final_expr_var = @as(TypeVar, @enumFromInt(@intFromEnum(final_expr.idx))); - return .{ - .stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }, - }; + // 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, }, - .@"var" => |v| { - defer last_type_anno.* = null; // See above comment for why this is necessary + }; + const block_idx = try self.env.addExprAndTypeVar(block_expr, Content{ .flex = types.Flex.init() }, block_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 }; +} + +const StatementResult = struct { + canonicalized_stmt: ?CanonicalizedStatement, + second_canonicalized_stmt: ?CanonicalizedStatement, +}; + +/// 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 mb_second_canonicalized_stmt: ?CanonicalizedStatement = null; + + 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, - } }; + }; + break :blk; }; // Canonicalize the initial value @@ -5932,32 +6264,26 @@ pub fn canonicalizeStatement( .expr = expr.idx, } }, ModuleEnv.varFrom(expr.idx), 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 = .{ .expr = expr.idx, } }, ModuleEnv.varFrom(expr.idx), 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| { @@ -5974,28 +6300,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.addStatementAndTypeVar(Statement{ .s_crash = .{ + .msg = msg_literal, + } }, .err, 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 = null }; }, .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 @@ -6007,28 +6333,22 @@ pub fn canonicalizeStatement( .expr = expr.idx, } }, ModuleEnv.varFrom(expr.idx), 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 = .{ .body = expr.idx, } }, Content{ .structure = .empty_record }, 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 const region = self.parse_ir.tokenizedRegionToRegion(r.region); // Canonicalize the return expression @@ -6039,26 +6359,18 @@ pub fn canonicalizeStatement( .expr = expr.idx, } }, ModuleEnv.varFrom(expr.idx), 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 - // 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 }, - }; + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = malformed_idx, .free_vars = null }; }, - .type_anno => |ta| { - // Note that we do _not_ defer resetting last_type_anno in this branch - + .type_anno => |ta| blk: { // Type annotation statement const region = self.parse_ir.tokenizedRegionToRegion(ta.region); @@ -6069,19 +6381,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 = null }; + 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(); @@ -6093,50 +6411,220 @@ 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: @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); - - last_type_anno.* = StmtTypeAnno{ - .anno_idx = type_anno_stmt_idx, - .anno = type_anno_stmt, + // If we have where clauses, create a separate s_type_anno statement + const mb_type_anno_stmt_idx: ?Statement.Idx = inner_blk: { + if (where_clauses != null) { + break :inner_blk try self.env.addStatementAndTypeVarRedirect(Statement{ + .s_type_anno = .{ + .name = name_ident, + .anno = type_anno_idx, + .where = where_clauses, + }, + }, ModuleEnv.varFrom(type_anno_idx), region); + } else { + break :inner_blk null; + } }; - return .{ - .stmt = CanonicalizedStatement{ .idx = type_anno_stmt_idx, .free_vars = null }, - }; + // Set the type annotation stmt if it exists + if (mb_type_anno_stmt_idx) |type_anno_stmt_idx| { + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = type_anno_stmt_idx, .free_vars = null }; + } + + // 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); + + switch (next_stmt) { + .decl => |decl| { + // Immediately process the next decl, with the annotation + mb_second_canonicalized_stmt = try self.canonicalizeBlockDecl(decl, TypeAnnoIdent{ + .name = name_ident, + .anno_idx = type_anno_idx, + }); + }, + else => {}, + } + } }, .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 - // 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 }, - }; + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = malformed_idx, .free_vars = null }; }, } + + return StatementResult{ .canonicalized_stmt = mb_canonicailzed_stmt, .second_canonicalized_stmt = mb_second_canonicalized_stmt }; +} + +/// 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.addStatementAndTypeVarRedirect(Statement{ .s_reassign = .{ + .pattern_idx = existing_pattern_idx, + .expr = malformed_idx, + } }, ModuleEnv.varFrom(malformed_idx), ident_region); + + return 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 CanonicalizedStatement{ .idx = reassign_idx, .free_vars = expr.free_vars }; + } + }, + .not_found => { + // Not found in scope, fall through to regular declaration + }, + } + } + }, + else => {}, + } + + // check against last anno + + // Get the last annotation, if it exists + 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, pattern_region); + } + } else { + // TODO: Diagnostic + } + } + } + + // 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); + + // Create a declaration statement + const stmt_idx = try self.env.addStatementAndTypeVarRedirect(Statement{ .s_decl = .{ + .pattern = pattern_idx, + .expr = expr.idx, + .anno = mb_validated_anno, + } }, ModuleEnv.varFrom(expr.idx), region); + + return CanonicalizedStatement{ .idx = stmt_idx, .free_vars = expr.free_vars }; +} + +// A canonicalized statement +const CanonicalizedStatement = struct { + idx: Statement.Idx, + free_vars: ?[]Pattern.Idx, +}; + +// 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(self.env.gpa, TypeVarScope{ .ident = name_ident, .anno_idx = type_var_anno }); + return .success; } // scope // @@ -6144,11 +6632,24 @@ 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; } @@ -6157,37 +6658,33 @@ pub fn scopeExit(self: *Self, gpa: std.mem.Allocator) Scope.Error!void { const scope = &self.scopes.items[self.scopes.items.len - 1]; 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 { 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( CIR.Expr{ .e_frac_f64 = .{ .value = value, + .has_suffix = false, }, }, - Content{ .structure = .{ .num = .{ .frac_unbound = frac_requirements } } }, + .err, region, ); @@ -6222,62 +6719,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); } } } @@ -6430,7 +6885,7 @@ pub fn scopeIntroduceInternal( } // 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 @@ -6470,7 +6925,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); @@ -6486,7 +6941,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 @@ -6553,6 +7008,23 @@ 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)); + 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); @@ -6708,6 +7180,12 @@ fn scopeLookupTypeDecl(self: *Self, ident_idx: Ident.Idx) ?Statement.Idx { i -= 1; const scope = &self.scopes.items[i]; + // Check for type aliases (unqualified names in associated blocks) + if (scope.lookupTypeAlias(ident_idx)) |aliased_decl| { + return aliased_decl; + } + + // Check regular type declarations switch (scope.lookupTypeDecl(ident_idx)) { .found => |type_decl_idx| return type_decl_idx, .not_found => continue, @@ -6958,7 +7436,7 @@ fn canonicalizeWhereClause(self: *Self, ast_where_idx: AST.WhereClause.Idx, type 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_type_var = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .{ .flex = types.Flex.init() }, 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 = .{ @@ -6967,7 +7445,7 @@ fn canonicalizeWhereClause(self: *Self, ast_where_idx: AST.WhereClause.Idx, type .args = args_span, .ret_anno = ret_anno, .external_decl = external_decl, - } }, .{ .flex_var = null }, region); + } }, .{ .flex = types.Flex.init() }, region); }, .mod_alias => |ma| { const region = self.parse_ir.tokenizedRegionToRegion(ma.region); @@ -7003,14 +7481,14 @@ fn canonicalizeWhereClause(self: *Self, ast_where_idx: AST.WhereClause.Idx, type 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_type_var = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .{ .flex = types.Flex.init() }, region, TypeVar); const external_decl = try self.createExternalDeclaration(qualified_name, module_name, alias_ident, .type, external_type_var, region); return try self.env.addWhereClauseAndTypeVar(WhereClause{ .mod_alias = .{ .var_name = var_ident, .alias_name = alias_ident, .external_decl = external_decl, - } }, .{ .flex_var = null }, region); + } }, .{ .flex = types.Flex.init() }, region); }, .malformed => |m| { const region = self.parse_ir.tokenizedRegionToRegion(m.region); @@ -7019,19 +7497,20 @@ fn canonicalizeWhereClause(self: *Self, ast_where_idx: AST.WhereClause.Idx, type } }, .err); return try self.env.addWhereClauseAndTypeVar(WhereClause{ .malformed = .{ .diagnostic = diagnostic, - } }, .{ .flex_var = null }, region); + } }, .{ .flex = types.Flex.init() }, 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, 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 + // TODO: Capture where clauses const annotation = CIR.Annotation{ .type_anno = type_anno_idx, .signature = try self.env.addTypeSlotAndTypeVar(@enumFromInt(0), .err, region, TypeVar), @@ -7117,7 +7596,7 @@ fn tryModuleQualifiedLookup(self: *Self, field_access: AST.BinOp) std.mem.Alloca .module_idx = import_idx, .target_node_idx = target_node_idx, .region = region, - } }, Content{ .flex_var = null }, region); + } }, Content{ .flex = types.Flex.init() }, region); return expr_idx; } @@ -7145,7 +7624,7 @@ fn canonicalizeRegularFieldAccess(self: *Self, field_access: AST.BinOp) std.mem. }, }; - 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.addExprAndTypeVar(dot_access_expr, Content{ .flex = types.Flex.init() }, self.parse_ir.tokenizedRegionToRegion(field_access.region)); return expr_idx; } @@ -7238,6 +7717,186 @@ 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; +} + +/// Expose all associated items of a type declaration (recursively for nested types) +/// This is used for type modules where all associated items are implicitly exposed +fn exposeAssociatedItems(self: *Self, parent_name: Ident.Idx, type_decl: anytype) std.mem.Allocator.Error!void { + if (type_decl.associated) |assoc| { + 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| { + // Get the nested type name + const nested_header = self.parse_ir.store.getTypeHeader(nested_type_decl.header) catch continue; + const nested_ident = self.parse_ir.tokens.resolveIdentifier(nested_header.name) orelse continue; + + // Build qualified name (e.g., "Foo.Bar") + const parent_text = self.env.getIdent(parent_name); + const nested_text = self.env.getIdent(nested_ident); + const qualified_name_str = try std.fmt.allocPrint( + self.env.gpa, + "{s}.{s}", + .{ parent_text, nested_text }, + ); + defer self.env.gpa.free(qualified_name_str); + const qualified_ident = base.Ident.for_text(qualified_name_str); + const qualified_idx = try self.env.insertIdent(qualified_ident); + + // Expose the nested type + try self.env.addExposedById(qualified_idx); + + // Recursively expose its associated items + try self.exposeAssociatedItems(qualified_idx, nested_type_decl); + }, + .decl => |decl| { + // Get the declaration 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| { + // Build qualified name (e.g., "Foo.stuff") + const parent_text = self.env.getIdent(parent_name); + const decl_text = self.env.getIdent(decl_ident); + const qualified_name_str = try std.fmt.allocPrint( + self.env.gpa, + "{s}.{s}", + .{ parent_text, decl_text }, + ); + defer self.env.gpa.free(qualified_name_str); + const qualified_ident = base.Ident.for_text(qualified_name_str); + const qualified_idx = try self.env.insertIdent(qualified_ident); + + // Expose the declaration + try self.env.addExposedById(qualified_idx); + } + } + }, + else => {}, + } + } + } +} + +/// 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/Diagnostic.zig b/src/canonicalize/Diagnostic.zig index e90b18d07f..0998dcd0ba 100644 --- a/src/canonicalize/Diagnostic.zig +++ b/src/canonicalize/Diagnostic.zig @@ -137,6 +137,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, @@ -233,6 +270,15 @@ pub const Diagnostic = union(enum) { .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, diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index 383118a4db..565fc52c0a 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 @@ -127,7 +133,6 @@ pub const Expr = union(enum) { /// ["one", "two", "three"] /// ``` e_list: struct { - elem_var: TypeVar, elems: Expr.Span, }, /// Empty list constant `[]` @@ -162,6 +167,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, }, @@ -466,15 +472,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 +521,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 +547,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; @@ -714,12 +719,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); } } diff --git a/src/canonicalize/ModuleEnv.zig b/src/canonicalize/ModuleEnv.zig index 1494816062..ca69755de4 100644 --- a/src/canonicalize/ModuleEnv.zig +++ b/src/canonicalize/ModuleEnv.zig @@ -29,6 +29,18 @@ 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, +}; + gpa: std.mem.Allocator, common: CommonEnv, @@ -37,12 +49,16 @@ types: TypeStore, // ===== 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, +/// 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 @@ -56,12 +72,32 @@ diagnostics: CIR.Diagnostic.Span, /// Uses an efficient data structure, and provides helpers for storing and retrieving nodes. store: NodeStore, +/// 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.imports.relocate(offset); + // Note: NodeStore.Serialized.deserialize() handles relocation internally, no separate relocate method needed + + // 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 + self.module_kind = undefined; // Set during 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; @@ -82,9 +118,11 @@ pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error! .gpa = gpa, .common = try CommonEnv.init(gpa, source), .types = try TypeStore.initCapacity(gpa, 2048, 512), + .module_kind = undefined, // Set during canonicalization .all_defs = .{ .span = .{ .start = 0, .len = 0 } }, .all_statements = .{ .span = .{ .start = 0, .len = 0 } }, .exports = .{ .span = .{ .start = 0, .len = 0 } }, + .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 @@ -956,6 +994,331 @@ pub fn diagnosticToReport(self: *Self, diagnostic: CIR.Diagnostic, allocator: st break :blk report; }, + .type_module_missing_matching_type => |data| blk: { + const region_info = self.calcRegionInfo(data.region); + + 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(); + + 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(); + + 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; + }, else => { // For unhandled diagnostics, create a generic report const diagnostic_name = @tagName(diagnostic); @@ -1001,73 +1364,21 @@ 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, - .exports = self.exports, - .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 + gpa: [2]u64, // Reserve space for allocator (vtable ptr + context ptr), provided during deserialization common: CommonEnv.Serialized, types: TypeStore.Serialized, all_defs: CIR.Def.Span, all_statements: CIR.Statement.Span, exports: CIR.Def.Span, + 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 diagnostics: CIR.Diagnostic.Span, store: NodeStore.Serialized, + module_kind: ModuleKind, /// Serialize a ModuleEnv into this Serialized struct, appending data to the writer pub fn serialize( @@ -1076,9 +1387,6 @@ 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); @@ -1086,6 +1394,7 @@ pub const Serialized = struct { self.all_defs = env.all_defs; self.all_statements = env.all_statements; self.exports = env.exports; + self.builtin_statements = env.builtin_statements; try self.external_decls.serialize(&env.external_decls, allocator, writer); try self.imports.serialize(&env.imports, allocator, writer); @@ -1095,9 +1404,10 @@ 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); + // Set gpa and module_name to all zeros; the space needs to be here, + // but the values will be set separately during deserialization. + self.gpa = .{ 0, 0 }; + self.module_name = .{ 0, 0 }; } /// Deserialize a ModuleEnv from the buffer, updating the ModuleEnv in place @@ -1117,10 +1427,12 @@ pub const Serialized = struct { env.* = Self{ .gpa = gpa, .common = self.common.deserialize(offset, source).*, - .types = self.types.deserialize(offset).*, + .types = self.types.deserialize(offset, gpa).*, + .module_kind = self.module_kind, .all_defs = self.all_defs, .all_statements = self.all_statements, .exports = self.exports, + .builtin_statements = self.builtin_statements, .external_decls = self.external_decls.deserialize(offset).*, .imports = self.imports.deserialize(offset, gpa).*, .module_name = module_name, @@ -1825,6 +2137,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); diff --git a/src/canonicalize/Node.zig b/src/canonicalize/Node.zig index efedb6a2de..4a9c28f538 100644 --- a/src/canonicalize/Node.zig +++ b/src/canonicalize/Node.zig @@ -58,7 +58,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, @@ -87,10 +87,12 @@ pub const Tag = enum { // 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, @@ -108,7 +110,6 @@ pub const Tag = enum { pattern_list, pattern_tuple, pattern_num_literal, - pattern_int_literal, pattern_dec_literal, pattern_f32_literal, pattern_f64_literal, @@ -163,6 +164,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, diff --git a/src/canonicalize/NodeStore.zig b/src/canonicalize/NodeStore.zig index bc9859969d..cc4717efb1 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; @@ -25,57 +26,91 @@ const FunctionArgs = base.FunctionArgs; 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), + pattern_record_fields: base.Scratch(CIR.PatternRecordField.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), + .pattern_record_fields = try base.Scratch(CIR.PatternRecordField.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(gpa); + self.exprs.deinit(gpa); + self.record_fields.deinit(gpa); + self.match_branches.deinit(gpa); + self.match_branch_patterns.deinit(gpa); + self.if_branches.deinit(gpa); + self.where_clauses.deinit(gpa); + self.patterns.deinit(gpa); + self.pattern_record_fields.deinit(gpa); + self.record_destructs.deinit(gpa); + self.type_annos.deinit(gpa); + self.anno_record_fields.deinit(gpa); + self.exposed_items.deinit(gpa); + self.defs.deinit(gpa); + self.diagnostics.deinit(gpa); + self.captures.deinit(gpa); + 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,29 +119,16 @@ 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); + } } /// 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 = 55; /// Count of the expression nodes in the ModuleEnv pub const MODULEENV_EXPR_NODE_COUNT = 33; /// Count of the statement nodes in the ModuleEnv @@ -323,21 +345,25 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { .region = store.getRegionAt(node_idx), } }; }, - .expr_int => { + .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 +377,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 +385,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 +421,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, }, }; }, @@ -815,19 +852,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 +867,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 +886,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 +896,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 +928,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 +942,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,7 +967,7 @@ 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 an pattern tag {}", .{node.tag}); }, } } @@ -958,31 +986,78 @@ pub fn getTypeAnno(store: *const NodeStore, typeAnno: CIR.TypeAnno.Idx) CIR.Type 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, } }, + .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 } }, } }, @@ -1002,12 +1077,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), } }, @@ -1077,7 +1146,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, @@ -1208,16 +1300,14 @@ pub fn addStatement(store: *NodeStore, statement: CIR.Statement, region: base.Re }, } - 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, @@ -1236,8 +1326,11 @@ 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_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 +1344,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 +1362,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 +1384,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; @@ -1396,8 +1492,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; @@ -1495,7 +1592,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 +1609,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 +1626,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 +1643,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 +1660,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 +1687,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,7 +1703,7 @@ 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, @@ -1653,7 +1750,7 @@ 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, @@ -1694,13 +1791,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 +1801,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 +1823,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 +1853,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; @@ -1792,7 +1886,7 @@ pub fn addPattern(store: *NodeStore, pattern: CIR.Pattern, region: base.Region) } /// Adds a pattern record field to the store. -pub fn addPatternRecordField(store: *NodeStore, patternRecordField: CIR.PatternRecordField) std.mem.Allocator.Error!CIR.PatternRecordField.Idx { +pub fn addPatternRecordField(store: *NodeStore, patternRecordField: CIR.PatternRecordField) Allocator.Error!CIR.PatternRecordField.Idx { _ = store; _ = patternRecordField; @@ -1803,7 +1897,7 @@ pub fn addPatternRecordField(store: *NodeStore, patternRecordField: CIR.PatternR /// /// 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, @@ -1813,30 +1907,60 @@ pub fn addTypeAnno(store: *NodeStore, typeAnno: CIR.TypeAnno, region: base.Regio 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; @@ -1844,6 +1968,12 @@ pub fn addTypeAnno(store: *NodeStore, typeAnno: CIR.TypeAnno, region: base.Regio node.data_3 = if (tu.ext) |ext| @intFromEnum(ext) 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; @@ -1867,11 +1997,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,7 +2012,7 @@ 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 { const node = Node{ .data_1 = @bitCast(typeHeader.name), .data_2 = typeHeader.args.span.start, @@ -1904,7 +2029,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,7 +2046,7 @@ 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 { +pub fn addAnnotation(store: *NodeStore, annotation: CIR.Annotation, region: base.Region) Allocator.Error!CIR.Annotation.Idx { const node = Node{ .data_1 = @intFromEnum(annotation.signature), .data_2 = @intFromEnum(annotation.type_anno), @@ -1938,7 +2063,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 +2080,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, @@ -2077,22 +2202,22 @@ pub fn getIfBranch(store: *const NodeStore, if_branch_idx: CIR.Expr.IfBranch.Idx /// 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(store.gpa, 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 +2232,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 +2277,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 +2377,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 +2488,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,7 +2559,7 @@ 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, @@ -2540,6 +2665,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; @@ -2694,7 +2865,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 +2886,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, @@ -2870,6 +3041,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), @@ -2962,13 +3170,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 +3187,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 +3202,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 +3218,49 @@ 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 { + 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.ArrayList(CIR.Statement.Idx) = .{}, - scratch_exprs: std.ArrayList(CIR.Expr.Idx) = .{}, - scratch_record_fields: std.ArrayList(CIR.RecordField.Idx) = .{}, - scratch_match_branches: std.ArrayList(CIR.Expr.Match.Branch.Idx) = .{}, - scratch_match_branch_patterns: std.ArrayList(CIR.Expr.Match.BranchPattern.Idx) = .{}, - scratch_if_branches: std.ArrayList(CIR.Expr.IfBranch.Idx) = .{}, - scratch_where_clauses: std.ArrayList(CIR.WhereClause.Idx) = .{}, - scratch_patterns: std.ArrayList(CIR.Pattern.Idx) = .{}, - scratch_pattern_record_fields: std.ArrayList(CIR.PatternRecordField.Idx) = .{}, - scratch_record_destructs: std.ArrayList(CIR.Pattern.RecordDestruct.Idx) = .{}, - scratch_type_annos: std.ArrayList(CIR.TypeAnno.Idx) = .{}, - scratch_anno_record_fields: std.ArrayList(CIR.TypeAnno.RecordField.Idx) = .{}, - scratch_exposed_items: std.ArrayList(CIR.ExposedItem.Idx) = .{}, - scratch_defs: std.ArrayList(CIR.Def.Idx) = .{}, - scratch_diagnostics: std.ArrayList(CIR.Diagnostic.Idx) = .{}, - scratch_captures: std.ArrayList(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,7 +3270,7 @@ pub const Serialized = struct { } /// Deserialize this Serialized struct into a NodeStore - pub fn deserialize(self: *Serialized, offset: i64, gpa: std.mem.Allocator) *NodeStore { + pub fn deserialize(self: *Serialized, offset: i64, gpa: Allocator) *NodeStore { // NodeStore.Serialized should be at least as big as NodeStore std.debug.assert(@sizeOf(Serialized) >= @sizeOf(NodeStore)); @@ -3141,23 +3282,7 @@ pub const Serialized = struct { .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 = .{} }, + .scratch = null, // A deserialized NodeStore is read-only, so it has no need for scratch memory! }; return store; @@ -3310,7 +3435,7 @@ test "NodeStore multiple nodes CompactWriter roundtrip" { .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); @@ -3383,7 +3508,6 @@ test "NodeStore multiple nodes CompactWriter roundtrip" { 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)); @@ -3401,8 +3525,6 @@ test "NodeStore multiple nodes CompactWriter roundtrip" { try testing.expectEqual(expected_region.end.offset, retrieved_region.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 942505fa5b..92478d83b5 100644 --- a/src/canonicalize/Pattern.zig +++ b/src/canonicalize/Pattern.zig @@ -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. @@ -435,14 +433,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(); diff --git a/src/canonicalize/Scope.zig b/src/canonicalize/Scope.zig index d0ea372c71..831513aa8b 100644 --- a/src/canonicalize/Scope.zig +++ b/src/canonicalize/Scope.zig @@ -15,6 +15,9 @@ 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), +/// Maps unqualified type names to their fully qualified Statement.Idx (for associated types) +/// Example: within Foo's associated block, "Bar" -> statement for "Foo.Bar" +type_aliases: std.AutoHashMapUnmanaged(Ident.Idx, CIR.Statement.Idx), /// 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 @@ -31,6 +34,7 @@ pub fn init(is_function_boundary: bool) 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){}, + .type_aliases = std.AutoHashMapUnmanaged(Ident.Idx, CIR.Statement.Idx){}, .type_vars = std.AutoHashMapUnmanaged(Ident.Idx, CIR.TypeAnno.Idx){}, .module_aliases = std.AutoHashMapUnmanaged(Ident.Idx, Ident.Idx){}, .exposed_items = std.AutoHashMapUnmanaged(Ident.Idx, ExposedItemInfo){}, @@ -44,6 +48,7 @@ pub fn deinit(self: *Scope, gpa: std.mem.Allocator) void { self.idents.deinit(gpa); self.aliases.deinit(gpa); self.type_decls.deinit(gpa); + self.type_aliases.deinit(gpa); self.type_vars.deinit(gpa); self.module_aliases.deinit(gpa); self.exposed_items.deinit(gpa); @@ -248,6 +253,22 @@ pub fn lookupTypeDecl(scope: *const Scope, name: Ident.Idx) TypeLookupResult { return TypeLookupResult{ .not_found = {} }; } +/// Look up an unqualified type alias (for associated types) +pub fn lookupTypeAlias(scope: *const Scope, name: Ident.Idx) ?CIR.Statement.Idx { + return scope.type_aliases.get(name); +} + +/// 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_aliases.put(gpa, unqualified_name, qualified_type_decl); +} + /// Update an existing type declaration in the scope /// This is used for recursive type declarations where we need to update /// the statement index after canonicalizing the type annotation diff --git a/src/canonicalize/TypeAnnotation.zig b/src/canonicalize/TypeAnnotation.zig index ccc49325f2..f732b2e46e 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. @@ -96,9 +106,41 @@ 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 ext_begin = tree.beginNode(); + try tree.pushStaticAtom("external"); + + // Add module index + var buf: [32]u8 = undefined; + const module_idx_str = std.fmt.bufPrint(&buf, "{}", .{@intFromEnum(external.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, "{}", .{external.target_node_idx}) catch unreachable; + try tree.pushStringPair("target-node-idx", target_idx_str); + + const field_attrs = tree.beginNode(); + try tree.endNode(ext_begin, field_attrs); + }, + } + + 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 +148,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 +172,45 @@ 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 ext_begin = tree.beginNode(); + try tree.pushStaticAtom("external"); + + // Add module index + var buf: [32]u8 = undefined; + const module_idx_str = std.fmt.bufPrint(&buf, "{}", .{@intFromEnum(external.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, "{}", .{external.target_node_idx}) catch unreachable; + try tree.pushStringPair("target-node-idx", target_idx_str); + + const field_attrs = tree.beginNode(); + try tree.endNode(ext_begin, field_attrs); + }, + } + const attrs = tree.beginNode(); try tree.endNode(begin, attrs); }, @@ -169,6 +232,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 +312,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"); @@ -273,16 +332,25 @@ pub const TypeAnno = union(enum) { pub const Span = struct { span: DataSpan }; }; - /// 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]) + /// 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 of an external type in a type annotation - pub const ApplyExternal = struct { - module_idx: CIR.Import.Idx, - target_node_idx: u16, + /// A type application in a type annotation + pub const Apply = struct { + name: Ident.Idx, // The type name + base: LocalOrExternal, // Reference to the type args: TypeAnno.Span, // The type arguments (e.g., [Str], [String, Int]) }; @@ -308,4 +376,75 @@ 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 { + str, + list, + box, + num, + frac, + int, + 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) { + .str => return "Str", + .list => return "List", + .box => return "Box", + .num => return "Num", + .frac => return "Frac", + .int => return "Int", + .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, "Str")) return .str; + if (std.mem.eql(u8, bytes, "List")) return .list; + if (std.mem.eql(u8, bytes, "Num")) return .num; + if (std.mem.eql(u8, bytes, "Frac")) return .frac; + if (std.mem.eql(u8, bytes, "Int")) return .int; + 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; + } + }; }; diff --git a/src/canonicalize/test/TestEnv.zig b/src/canonicalize/test/TestEnv.zig index 86c7a3c13a..d2a6d104b6 100644 --- a/src/canonicalize/test/TestEnv.zig +++ b/src/canonicalize/test/TestEnv.zig @@ -50,7 +50,7 @@ pub fn init(source: []const u8) !TestEnv { try module_env.initCIRFields(gpa, "test"); - can.* = try Can.init(module_env, parse_ast, null); + can.* = try Can.init(module_env, parse_ast, null, .{}); return TestEnv{ .gpa = gpa, diff --git a/src/canonicalize/test/exposed_shadowing_test.zig b/src/canonicalize/test/exposed_shadowing_test.zig index 35d7a70747..5981079f61 100644 --- a/src/canonicalize/test/exposed_shadowing_test.zig +++ b/src/canonicalize/test/exposed_shadowing_test.zig @@ -32,15 +32,15 @@ test "exposed but not implemented - values" { var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer.deinit(); try czer.canonicalizeFile(); // 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| { @@ -71,15 +71,15 @@ test "exposed but not implemented - types" { var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer.deinit(); try czer.canonicalizeFile(); // 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| { @@ -108,7 +108,7 @@ test "redundant exposed entries" { try env.initCIRFields(allocator, "Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer .deinit(); try czer @@ -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| { @@ -151,15 +151,15 @@ test "shadowing with exposed items" { try env.initCIRFields(allocator, "Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer .deinit(); try czer .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, @@ -184,15 +184,15 @@ test "shadowing non-exposed items" { try env.initCIRFields(allocator, "Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer .deinit(); try czer .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| { @@ -224,7 +224,7 @@ test "exposed items correctly tracked across shadowing" { try env.initCIRFields(allocator, "Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer .deinit(); try czer @@ -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| { @@ -280,7 +280,7 @@ test "complex case with redundant, shadowing, and not implemented" { try env.initCIRFields(allocator, "Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer .deinit(); try czer @@ -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| { @@ -332,7 +332,7 @@ test "exposed_items is populated correctly" { try env.initCIRFields(allocator, "Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer .deinit(); try czer @@ -364,7 +364,7 @@ test "exposed_items persists after canonicalization" { try env.initCIRFields(allocator, "Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer .deinit(); try czer @@ -394,7 +394,7 @@ test "exposed_items never has entries removed" { try env.initCIRFields(allocator, "Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer .deinit(); try czer @@ -427,7 +427,7 @@ test "exposed_items handles identifiers with different attributes" { try env.initCIRFields(allocator, "Test"); var ast = try parse.parse(&env.common, allocator); defer ast.deinit(allocator); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer .deinit(); try czer diff --git a/src/canonicalize/test/frac_test.zig b/src/canonicalize/test/frac_test.zig index bff445686d..3d2c03ca2c 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,60 +27,11 @@ 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; - }, - } + try testing.expectEqual(dec.value.numerator, 314); + try testing.expectEqual(dec.value.denominator_power_of_ten, 2); }, - .e_frac_dec => |dec| { + .e_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; - }, - } }, else => { std.debug.print("Unexpected expr type: {}\n", .{expr}); @@ -102,89 +52,13 @@ 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 => |frac| { // 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 +78,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 +117,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 +149,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 +226,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 +246,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 +292,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 +329,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 +349,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 +376,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 +395,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 +417,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 +436,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 +455,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 +474,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 +493,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 +513,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 +531,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 +554,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 +573,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 +593,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 +613,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 +632,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/import_store_test.zig b/src/canonicalize/test/import_store_test.zig index 7fe433db82..d330918da2 100644 --- a/src/canonicalize/test/import_store_test.zig +++ b/src/canonicalize/test/import_store_test.zig @@ -12,31 +12,30 @@ const CompactWriter = collections.CompactWriter; 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 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()); @@ -50,13 +49,9 @@ test "Import.Store deduplicates module names" { 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(.{}); @@ -66,13 +61,13 @@ 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); @@ -81,7 +76,7 @@ test "Import.Store empty CompactWriter roundtrip" { // 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 = serialized_ptr.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr))), gpa); // Verify empty try testing.expectEqual(@as(usize, 0), deserialized.imports.len()); @@ -91,22 +86,21 @@ 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"); + const idx1 = try original.getOrPut(gpa, mock_env.strings, "json.Json"); + const idx2 = try original.getOrPut(gpa, mock_env.strings, "core.List"); + const idx3 = try original.getOrPut(gpa, mock_env.strings, "my.Module"); // Verify indices try testing.expectEqual(@as(u32, 0), @intFromEnum(idx1)); @@ -121,13 +115,13 @@ 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); @@ -136,7 +130,8 @@ test "Import.Store basic CompactWriter roundtrip" { // 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 = serialized_ptr.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr))), gpa); + defer deserialized.map.deinit(gpa); // Verify the imports are accessible try testing.expectEqual(@as(usize, 3), deserialized.imports.len()); @@ -157,22 +152,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, mock_env.strings, "test.Module"); + const idx2 = try original.getOrPut(gpa, mock_env.strings, "another.Module"); + const idx3 = try original.getOrPut(gpa, mock_env.strings, "test.Module"); // duplicate // Verify deduplication worked try testing.expectEqual(idx1, idx3); @@ -186,13 +180,13 @@ 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); @@ -201,7 +195,8 @@ test "Import.Store duplicate imports CompactWriter roundtrip" { // 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 = serialized_ptr.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr))), gpa); + defer deserialized.map.deinit(gpa); // Verify correct number of imports try testing.expectEqual(@as(usize, 2), deserialized.imports.len()); diff --git a/src/canonicalize/test/import_validation_test.zig b/src/canonicalize/test/import_validation_test.zig index 521c41396d..028faff23e 100644 --- a/src/canonicalize/test/import_validation_test.zig +++ b/src/canonicalize/test/import_validation_test.zig @@ -18,12 +18,18 @@ 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.StringHashMap(*const ModuleEnv), +) !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); @@ -33,7 +39,7 @@ fn parseAndCanonicalizeSource(allocator: std.mem.Allocator, source: []const u8, try parse_env.initCIRFields(allocator, "Test"); const can = try allocator.create(Can); - can.* = try Can.init(parse_env, ast, module_envs); + can.* = try Can.init(parse_env, ast, module_envs, .{}); return .{ .parse_env = parse_env, @@ -46,8 +52,9 @@ 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); + var module_envs = std.StringHashMap(*const ModuleEnv).init(allocator); defer module_envs.deinit(); // Create module environment for "Json" module const json_env = try allocator.create(ModuleEnv); @@ -112,7 +119,7 @@ test "import validation - mix of MODULE NOT FOUND, TYPE NOT EXPOSED, VALUE NOT E // Initialize CIR fields try parse_env.initCIRFields(allocator, "Test"); // Canonicalize with module validation - var can = try Can.init(parse_env, &ast, &module_envs); + var can = try Can.init(parse_env, &ast, &module_envs, .{}); defer can.deinit(); _ = try can.canonicalizeFile(); // Collect all diagnostics @@ -165,6 +172,7 @@ 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] @@ -186,7 +194,7 @@ test "import validation - no module_envs provided" { try parse_env.initCIRFields(allocator, "Test"); // Create czer // with null module_envs - var can = try Can.init(parse_env, &ast, null); + var can = try Can.init(parse_env, &ast, null, .{}); defer can.deinit(); _ = try can.canonicalizeFile(); const diagnostics = try parse_env.getDiagnostics(); @@ -196,6 +204,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); @@ -457,8 +468,9 @@ 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.StringHashMap(*const ModuleEnv).init(allocator); defer module_envs.deinit(); // Create a "MathUtils" module with some exposed definitions const math_env = try allocator.create(ModuleEnv); @@ -580,6 +592,7 @@ 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 diff --git a/src/canonicalize/test/int_test.zig b/src/canonicalize/test/int_test.zig index e8c4080b19..f79dc10e40 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 }){}; @@ -507,7 +480,7 @@ test "hexadecimal integer literals" { var ast = try parse.parseExpr(&env.common, env.gpa); defer ast.deinit(gpa); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer.deinit(); const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx); @@ -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 }){}; @@ -596,7 +539,7 @@ test "binary integer literals" { var ast = try parse.parseExpr(&env.common, env.gpa); defer ast.deinit(gpa); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer.deinit(); const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx); @@ -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 }){}; @@ -685,7 +598,7 @@ test "octal integer literals" { var ast = try parse.parseExpr(&env.common, env.gpa); defer ast.deinit(gpa); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer.deinit(); const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx); @@ -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 }){}; @@ -774,7 +657,7 @@ test "integer literals with uppercase base prefixes" { var ast = try parse.parseExpr(&env.common, gpa); defer ast.deinit(gpa); - var czer = try Can.init(&env, &ast, null); + var czer = try Can.init(&env, &ast, null, .{}); defer czer.deinit(); const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx); @@ -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))); } } @@ -834,39 +689,18 @@ test "numeric literal patterns use pattern idx as type var" { // 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.addPatternAndTypeVar(int_pattern, .err, 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 @@ -880,302 +714,17 @@ test "numeric literal patterns use pattern idx as type var" { 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.addPatternAndTypeVar(dec_pattern, .err, 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 => {}, - } } } @@ -1193,23 +742,25 @@ test "pattern numeric literal value edge cases" { // 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 @@ -1221,9 +772,11 @@ test "pattern numeric literal value edge cases" { 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 @@ -1245,7 +798,7 @@ test "pattern numeric literal value edge cases" { const dec_pattern = CIR.Pattern{ .dec_literal = .{ .value = RocDec{ .num = 314159265358979323 }, // π * 10^17 - + .has_suffix = false, }, }; @@ -1267,6 +820,7 @@ test "pattern numeric literal value edge cases" { 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 { @@ -1670,3 +1017,330 @@ test "hex literal parsing logic integration" { try std.testing.expectEqual(tc.expected_value, u128_val); } } + +// number req tests // +// TODO: Review, claude generated + +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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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.Num.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 9ef94383a9..1de617d9d0 100644 --- a/src/canonicalize/test/node_store_test.zig +++ b/src/canonicalize/test/node_store_test.zig @@ -54,6 +54,7 @@ fn rand_region() base.Region { test "NodeStore round trip - Statements" { const gpa = testing.allocator; + var store = try NodeStore.init(gpa); defer store.deinit(); @@ -175,6 +176,7 @@ test "NodeStore round trip - Statements" { test "NodeStore round trip - Expressions" { const gpa = testing.allocator; + var store = try NodeStore.init(gpa); defer store.deinit(); @@ -182,25 +184,30 @@ test "NodeStore round trip - Expressions" { defer expressions.deinit(); try expressions.append(CIR.Expr{ - .e_int = .{ + .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) }, + .e_frac_f32 = .{ .value = rand.random().float(f32), .has_suffix = false }, }); try expressions.append(CIR.Expr{ - .e_frac_f64 = .{ .value = rand.random().float(f64) }, + .e_frac_f64 = .{ .value = rand.random().float(f64), .has_suffix = false }, }); try expressions.append(CIR.Expr{ - .e_frac_dec = .{ + .e_dec = .{ .value = RocDec{ .num = 314 }, + .has_suffix = false, }, }); try expressions.append(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{ @@ -227,7 +234,6 @@ test "NodeStore round trip - Expressions" { }); try expressions.append(CIR.Expr{ .e_list = .{ - .elem_var = rand_idx(TypeVar), .elems = CIR.Expr.Span{ .span = rand_span() }, }, }); @@ -251,6 +257,7 @@ test "NodeStore round trip - Expressions" { }); try expressions.append(CIR.Expr{ .e_call = .{ + .func = rand_idx(CIR.Expr.Idx), .args = CIR.Expr.Span{ .span = rand_span() }, .called_via = CalledVia.apply, }, @@ -347,8 +354,9 @@ test "NodeStore round trip - Expressions" { }, }); try expressions.append(CIR.Expr{ - .e_frac_dec = .{ + .e_dec = .{ .value = RocDec{ .num = 123456789 }, + .has_suffix = false, }, }); try expressions.append(CIR.Expr{ @@ -385,6 +393,7 @@ test "NodeStore round trip - Expressions" { test "NodeStore round trip - Diagnostics" { const gpa = testing.allocator; + var store = try NodeStore.init(gpa); defer store.deinit(); @@ -547,6 +556,70 @@ test "NodeStore round trip - Diagnostics" { }, }); + try diagnostics.append(CIR.Diagnostic{ + .type_module_missing_matching_type = .{ + .module_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(CIR.Diagnostic{ + .default_app_missing_main = .{ + .module_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(CIR.Diagnostic{ + .default_app_wrong_arity = .{ + .arity = 2, + .region = rand_region(), + }, + }); + + try diagnostics.append(CIR.Diagnostic{ + .cannot_import_default_app = .{ + .module_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(CIR.Diagnostic{ + .execution_requires_app_or_default_app = .{ + .region = rand_region(), + }, + }); + + try diagnostics.append(CIR.Diagnostic{ + .type_name_case_mismatch = .{ + .module_name = rand_ident_idx(), + .type_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(CIR.Diagnostic{ + .module_header_deprecated = .{ + .region = rand_region(), + }, + }); + + try diagnostics.append(CIR.Diagnostic{ + .redundant_expose_main_type = .{ + .type_name = rand_ident_idx(), + .module_name = rand_ident_idx(), + .region = rand_region(), + }, + }); + + try diagnostics.append(CIR.Diagnostic{ + .invalid_main_type_rename_in_exposing = .{ + .type_name = rand_ident_idx(), + .alias = rand_ident_idx(), + .region = rand_region(), + }, + }); + try diagnostics.append(CIR.Diagnostic{ .unused_variable = .{ .ident = rand_ident_idx(), @@ -732,6 +805,7 @@ test "NodeStore round trip - Diagnostics" { test "NodeStore round trip - TypeAnno" { const gpa = testing.allocator; + var store = try NodeStore.init(gpa); defer store.deinit(); @@ -741,30 +815,63 @@ test "NodeStore round trip - TypeAnno" { // Test all TypeAnno variants to ensure complete coverage try type_annos.append(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(CIR.TypeAnno{ + .apply = .{ + .name = rand_ident_idx(), + .base = .{ .local = .{ .decl_idx = @enumFromInt(10) } }, + .args = CIR.TypeAnno.Span{ .span = rand_span() }, + }, + }); + try type_annos.append(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 = .{ + .rigid_var = .{ .name = rand_ident_idx(), }, }); + try type_annos.append(CIR.TypeAnno{ + .rigid_var_lookup = .{ + .ref = rand_idx(CIR.TypeAnno.Idx), + }, + }); try type_annos.append(CIR.TypeAnno{ .underscore = {}, }); try type_annos.append(CIR.TypeAnno{ - .ty = .{ - .symbol = rand_ident_idx(), + .lookup = .{ + .name = rand_ident_idx(), + .base = .{ .builtin = .dec }, }, }); - try type_annos.append(CIR.TypeAnno{ - .ty = .{ - .symbol = rand_ident_idx(), + .lookup = .{ + .name = rand_ident_idx(), + .base = .{ .local = .{ .decl_idx = @enumFromInt(10) } }, + }, + }); + try type_annos.append(CIR.TypeAnno{ + .lookup = .{ + .name = rand_ident_idx(), + .base = .{ .external = .{ + .module_idx = rand_idx(CIR.Import.Idx), + .target_node_idx = rand.random().int(u16), + } }, }, }); @@ -775,6 +882,13 @@ test "NodeStore round trip - TypeAnno" { }, }); + try type_annos.append(CIR.TypeAnno{ + .tag = .{ + .name = rand_ident_idx(), + .args = CIR.TypeAnno.Span{ .span = rand_span() }, + }, + }); + try type_annos.append(CIR.TypeAnno{ .tuple = .{ .elems = CIR.TypeAnno.Span{ .span = rand_span() }, @@ -801,19 +915,6 @@ test "NodeStore round trip - TypeAnno" { }, }); - 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{ .malformed = .{ .diagnostic = rand_idx(CIR.Diagnostic.Idx), @@ -833,8 +934,13 @@ test "NodeStore round trip - TypeAnno" { }; } + // 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) { + if (actual_test_count - extra_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", .{}); return error.IncompleteTypeAnnoTestCoverage; @@ -843,6 +949,7 @@ test "NodeStore round trip - TypeAnno" { test "NodeStore round trip - Pattern" { const gpa = testing.allocator; + var store = try NodeStore.init(gpa); defer store.deinit(); @@ -884,15 +991,11 @@ test "NodeStore round trip - Pattern" { }); try patterns.append(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{ .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) }, }, @@ -903,22 +1006,27 @@ test "NodeStore round trip - Pattern" { }, }); try patterns.append(CIR.Pattern{ - .int_literal = .{ + .num_literal = .{ .value = CIR.IntValue{ .bytes = @bitCast(rand.random().int(i128)), .kind = .i128, }, + .kind = .int_unbound, }, }); try patterns.append(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{ .dec_literal = .{ .value = RocDec.fromU64(rand.random().int(u64)), + .has_suffix = false, }, }); try patterns.append(CIR.Pattern{ diff --git a/src/canonicalize/test/record_test.zig b/src/canonicalize/test/record_test.zig index 87ee4cd896..fffcc2337c 100644 --- a/src/canonicalize/test/record_test.zig +++ b/src/canonicalize/test/record_test.zig @@ -26,7 +26,7 @@ test "record literal uses record_unbound" { var ast = try parse.parseExpr(&env.common, gpa); defer ast.deinit(gpa); - var can = try Can.init(&env, &ast, null); + var can = try Can.init(&env, &ast, null, .{}); defer can.deinit(); const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx); @@ -63,7 +63,7 @@ test "record literal uses record_unbound" { var ast = try parse.parseExpr(&env.common, gpa); defer ast.deinit(gpa); - var can = try Can.init(&env, &ast, null); + var can = try Can.init(&env, &ast, null, .{}); defer can.deinit(); const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx); @@ -100,7 +100,7 @@ test "record literal uses record_unbound" { var ast = try parse.parseExpr(&env.common, gpa); defer ast.deinit(gpa); - var can = try Can.init(&env, &ast, null); + var can = try Can.init(&env, &ast, null, .{}); defer can.deinit(); const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx); @@ -133,6 +133,7 @@ test "record literal uses record_unbound" { test "record_unbound basic functionality" { const gpa = std.testing.allocator; + const source = "{ x: 42, y: 99 }"; // Test that record literals create record_unbound types @@ -144,7 +145,7 @@ test "record_unbound basic functionality" { var ast = try parse.parseExpr(&env.common, gpa); defer ast.deinit(gpa); - var can = try Can.init(&env, &ast, null); + var can = try Can.init(&env, &ast, null, .{}); defer can.deinit(); const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx); @@ -176,6 +177,7 @@ test "record_unbound basic functionality" { 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); @@ -187,7 +189,7 @@ test "record_unbound with multiple fields" { var ast = try parse.parseExpr(&env.common, gpa); defer ast.deinit(gpa); - var can = try Can.init(&env, &ast, null); + var can = try Can.init(&env, &ast, null, .{}); defer can.deinit(); const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx); @@ -248,7 +250,7 @@ test "record with extension variable" { // Check that extension is a flex var (open record) const ext_resolved = env.types.resolveVar(record.ext); switch (ext_resolved.desc.content) { - .flex_var => { + .flex => { // Success! The record has an open extension }, else => return error.ExpectedFlexVar, diff --git a/src/canonicalize/test/scope_test.zig b/src/canonicalize/test/scope_test.zig index 39b0cfc3e1..97aed925df 100644 --- a/src/canonicalize/test/scope_test.zig +++ b/src/canonicalize/test/scope_test.zig @@ -26,7 +26,7 @@ const ScopeTestContext = struct { try module_env.initCIRFields(gpa, "test"); return ScopeTestContext{ - .self = try Can.init(module_env, undefined, null), + .self = try Can.init(module_env, undefined, null, .{}), .module_env = module_env, .gpa = gpa, }; diff --git a/src/check/Check.zig b/src/check/Check.zig index 73b3dbc775..cb55e1d550 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -9,6 +9,7 @@ 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"); @@ -21,16 +22,146 @@ const ModuleEnv = can.ModuleEnv; const Allocator = std.mem.Allocator; const Ident = base.Ident; const Region = base.Region; -const Instantiate = types_mod.instantiate.Instantiate; 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 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 Self = @This(); +gpa: std.mem.Allocator, +// not owned +types: *types_mod.Store, +cir: *ModuleEnv, +regions: *Region.List, +other_modules: []const *const ModuleEnv, +common_idents: CommonIdents, + +/// type snapshots used in error messages +snapshots: SnapshotStore, +/// type problems +problems: ProblemStore, +/// 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), +/// pool of variables that need to be generalized, built up during checking +var_pool: VarPool, +/// 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), +// 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), + +/// 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 struct of common idents +pub const CommonIdents = struct { + module_name: base.Ident.Idx, + list: base.Ident.Idx, + box: base.Ident.Idx, +}; + +/// 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 *const ModuleEnv, + regions: *Region.List, + common_idents: CommonIdents, +) std.mem.Allocator.Error!Self { + return .{ + .gpa = gpa, + .types = types, + .cir = @constCast(cir), + .other_modules = other_modules, + .regions = regions, + .common_idents = common_idents, + .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), + .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), + .var_pool = try VarPool.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), + .import_cache = ImportCache{}, + .constraint_origins = std.AutoHashMap(Var, Var).init(gpa), + }; +} + +/// 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.anno_free_vars.deinit(self.gpa); + self.decl_free_vars.deinit(self.gpa); + self.seen_annos.deinit(); + self.var_pool.deinit(); + self.generalizer.deinit(); + self.var_map.deinit(); + self.rigid_var_substitutions.deinit(self.gpa); + self.scratch_vars.deinit(self.gpa); + self.scratch_tags.deinit(self.gpa); + self.scratch_record_fields.deinit(self.gpa); + self.import_cache.deinit(self.gpa); + self.constraint_origins.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 }, + ); + } + } +} + +// import caches // + /// Key for the import cache: module index + expression index in that module const ImportCacheKey = struct { module_idx: CIR.Import.Idx, @@ -66,84 +197,10 @@ 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), - -/// 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), - }; -} - -/// 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(); -} - -/// 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 }, - ); - } - } -} - // unify // /// Unify two types -pub fn unify(self: *Self, a: Var, b: Var) std.mem.Allocator.Error!unifier.Result { +pub fn unify(self: *Self, a: Var, b: Var, rank: Rank) std.mem.Allocator.Error!unifier.Result { const trace = tracy.trace(@src()); defer trace.end(); @@ -175,6 +232,10 @@ pub fn unify(self: *Self, a: Var, b: Var) std.mem.Allocator.Error!unifier.Result } } + for (self.unify_scratch.fresh_vars.items.items) |fresh_var| { + try self.var_pool.addVarToRank(fresh_var, rank); + } + return result; } @@ -208,7 +269,7 @@ fn findConstraintOriginForVars(self: *Self, a: Var, b: Var) ?Var { /// 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 { +pub fn unifyFromAnno(self: *Self, a: Var, b: Var, rank: Rank) std.mem.Allocator.Error!unifier.Result { const trace = tracy.trace(@src()); defer trace.end(); @@ -235,6 +296,10 @@ pub fn unifyWithAnnotation(self: *Self, a: Var, b: Var) std.mem.Allocator.Error! try self.constraint_origins.put(a, origin); try self.constraint_origins.put(b, origin); } + + for (self.unify_scratch.fresh_vars.items.items) |fresh_var| { + try self.var_pool.addVarToRank(fresh_var, rank); + } } return result; @@ -260,7 +325,7 @@ pub fn unifyWithConstraintOrigin(self: *Self, a: Var, b: Var, constraint_origin_ ); } -// instantiate // +// instantiate // const InstantiateRegionBehavior = union(enum) { explicit: Region, @@ -268,36 +333,94 @@ 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, + rank: types_mod.Rank, 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 = rank, + .rigid_behavior = .fresh_flex, }; - const instantiated_var = try instantiate.instantiateVar(var_to_instantiate, &instantiate_ctx); + return self.instantiateVarHelp(var_to_instantiate, &instantiate_ctx, 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, + rank: types_mod.Rank, + 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 = rank, + .rigid_behavior = .fresh_flex, + }; + return self.instantiateVarHelp(var_to_instantiate, &instantiate_ctx, region_behavior); +} + +/// Instantiate a variable +fn instantiateVarWithSubs( + self: *Self, + var_to_instantiate: Var, + subs: *std.AutoHashMapUnmanaged(Ident.Idx, Var), + rank: types_mod.Rank, + 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 = rank, + .rigid_behavior = .{ .substitute_rigids = subs }, + }; + return self.instantiateVarHelp(var_to_instantiate, &instantiate_ctx, region_behavior); +} + +/// Instantiate a variable +fn instantiateVarHelp( + self: *Self, + var_to_instantiate: Var, + instantiator: *Instantiator, + 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); + // Add to pool + try self.var_pool.addVarToRank(fresh_var, instantiator.current_rank); + + // Set the region + try self.fillInRegionsThrough(fresh_var); switch (region_behavior) { .explicit => |region| { self.setRegionAt(fresh_var, region); @@ -317,58 +440,10 @@ fn instantiateVar( // 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,897 +460,2157 @@ 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 { + 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(); +fn fresh(self: *Self, rank: Rank, new_region: Region) Allocator.Error!Var { + const var_ = try self.types.freshWithRank(rank); try self.fillInRegionsThrough(var_); self.setRegionAt(var_, new_region); 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); +fn freshRedirect(self: *Self, redirect_to: Var, new_region: Region) Allocator.Error!Var { + const var_ = try self.types.freshRedirect(redirect_to); try self.fillInRegionsThrough(var_); self.setRegionAt(var_, new_region); return var_; } -// external types // - -const ExternalType = struct { - local_var: Var, - other_cir_node_idx: CIR.Node.Idx, - other_cir: *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, - 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; - - // 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 = 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; - }; - - return .{ - .local_var = copied_var, - .other_cir_node_idx = target_node_idx, - .other_cir = other_module_env, - }; - } else { - return null; - } +/// The the region for a variable +fn freshFromContent(self: *Self, content: Content, rank: types_mod.Rank, new_region: Region) Allocator.Error!Var { + const var_ = try self.types.freshFromContentWithRank(content, rank); + try self.fillInRegionsThrough(var_); + self.setRegionAt(var_, new_region); + return var_; } -// defs // +/// The the region for a variable +fn freshBool(self: *Self, rank: Rank, new_region: Region) Allocator.Error!Var { + // Look up Bool's actual index from builtin_statements (should be first) + const builtin_stmts_slice = self.cir.store.sliceStatements(self.cir.builtin_statements); + std.debug.assert(builtin_stmts_slice.len >= 1); // Must have at least Bool + const bool_stmt_idx = builtin_stmts_slice[0]; // Bool is always the first builtin + // Debug assertion: verify this is a nominal type declaration + if (std.debug.runtime_safety) { + const stmt = self.cir.store.getStatement(bool_stmt_idx); + std.debug.assert(stmt == .s_nominal_decl); + } + return try self.instantiateVar(ModuleEnv.varFrom(bool_stmt_idx), rank, .{ .explicit = new_region }); +} + +// fresh vars // + +fn updateVar(self: *Self, target_var: Var, content: types_mod.Content, rank: types_mod.Rank) std.mem.Allocator.Error!void { + try self.types.setVarDesc(target_var, .{ .content = content, .rank = rank, .mark = types_mod.Mark.none }); +} + +// file // /// Check the types for all defs -pub fn checkDefs(self: *Self) std.mem.Allocator.Error!void { +pub fn checkFile(self: *Self) std.mem.Allocator.Error!void { const trace = tracy.trace(@src()); defer trace.end(); + // First, iterate over the 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); + } + + // First, iterate over the statements, generating types for each type declaration + const stmts_slice = self.cir.store.sliceStatements(self.cir.all_statements); + for (stmts_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); + } + const defs_slice = self.cir.store.sliceDefs(self.cir.all_defs); for (defs_slice) |def_idx| { try self.checkDef(def_idx); } + + // Freeze interners after type-checking is complete + self.cir.freezeInterners(); } +// repl // + +/// Check an expr for the repl +pub fn checkExprRepl(self: *Self, expr_idx: CIR.Expr.Idx) std.mem.Allocator.Error!void { + // 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); + } + + // Push the rank for this definition + try self.var_pool.pushRank(); + defer self.var_pool.popRank(); + + // Ensure that the current rank in the pool is top-level + const rank = types_mod.Rank.top_level; + std.debug.assert(rank == self.var_pool.current_rank); + + _ = try self.checkExpr(expr_idx, rank, .no_expectation); + + // Now that we are existing the scope, we must generalize then pop this rank + try self.generalizer.generalize(&self.var_pool, rank); +} + +// defs // + /// Check the types for a single definition fn checkDef(self: *Self, def_idx: CIR.Def.Idx) std.mem.Allocator.Error!void { const trace = tracy.trace(@src()); defer trace.end(); + // Push the rank for this definition + try self.var_pool.pushRank(); + defer self.var_pool.popRank(); + + // Ensure that the current rank in the pool is top-level + const rank = types_mod.Rank.top_level; + std.debug.assert(rank == self.var_pool.current_rank); + 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)); + const def_var = ModuleEnv.varFrom(def_idx); + const ptrn_var = ModuleEnv.varFrom(def.pattern); + const expr_var = ModuleEnv.varFrom(def.expr); // Check the pattern - try self.checkPattern(def.pattern); - - // Get the defs var slot - const def_var = ModuleEnv.varFrom(def_idx); + try self.checkPattern(def.pattern, rank, .no_expectation); // 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); + self.anno_free_vars.items.clearRetainingCapacity(); + try self.generateAnnoTypeInPlace(annotation.type_anno, .annotation); + + // TODO: Duplicate anno var so if the body results in type mismatch, the + // annotation isn't corrupted + // We can instantiate, but how do we deal with rigid vars? + // const anno_var = try self.instantiateVaPreserveRigids(ModuleEnv.varFrom(annotation.type_anno), Rank.generalized, .use_last_var); 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); - } + _ = try self.checkExpr(def.expr, rank, .{ + .expected = .{ .var_ = anno_var, .from_annotation = true }, + }); } else { - // Check the expr - _ = try self.checkExpr(def.expr); + _ = try self.checkExpr(def.expr, rank, .no_expectation); } - // Unify the def with its expression - _ = try self.unify(def_var, ModuleEnv.varFrom(def.expr)); + // Also unify the pattern with the expr - needed so lookups work correctly + _ = try self.unify(ptrn_var, expr_var, rank); - // 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); + // Set the def var to redirect to the pattern + _ = try self.types.setVarRedirect(def_var, ptrn_var); + + // Now that we are existing the scope, we must generalize then pop this rank + try self.generalizer.generalize(&self.var_pool, rank); +} + +// 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, +) 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| { + // 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 + for (header_args) |header_arg_idx| { + const header_arg = self.cir.store.getTypeAnno(header_arg_idx); + const header_var = ModuleEnv.varFrom(header_arg_idx); + switch (header_arg) { + .rigid_var => |rigid| { + try self.updateVar(header_var, .{ .rigid = Rigid.init(rigid.name) }, Rank.generalized); + }, + .underscore, .malformed => { + try self.updateVar(header_var, .err, Rank.generalized); + }, + else => { + // This should never be possible + std.debug.assert(false); + try self.updateVar(header_var, .err, Rank.generalized); + }, + } + } + + const header_vars: []Var = @ptrCast(header_args); + + // 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, .{ .type_decl = .{ + .idx = decl_idx, + .name = header.name, + .type_ = .alias, + .backing_var = backing_var, + .num_args = @intCast(header_args.len), + } }); + + try self.updateVar( + decl_var, + try self.types.mkAlias( + .{ .ident_idx = header.name }, + backing_var, + header_vars, + ), + Rank.generalized, + ); + }, + .s_nominal_decl => |nominal| { + // 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 + for (header_args) |header_arg_idx| { + const header_arg = self.cir.store.getTypeAnno(header_arg_idx); + const header_var = ModuleEnv.varFrom(header_arg_idx); + switch (header_arg) { + .rigid_var => |rigid| { + try self.updateVar(header_var, .{ .rigid = Rigid.init(rigid.name) }, Rank.generalized); + }, + .underscore, .malformed => { + try self.updateVar(header_var, .err, Rank.generalized); + }, + else => { + // This should never be possible + std.debug.assert(false); + try self.updateVar(header_var, .err, Rank.generalized); + }, + } + } + + const header_vars: []Var = @ptrCast(header_args); + + // 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, .{ .type_decl = .{ + .idx = decl_idx, + .name = header.name, + .type_ = .nominal, + .backing_var = backing_var, + .num_args = @intCast(header_args.len), + } }); + + try self.updateVar( + decl_var, + try self.types.mkNominal( + .{ .ident_idx = header.name }, + backing_var, + header_vars, + self.common_idents.module_name, + ), + Rank.generalized, + ); + }, + .s_runtime_error => { + try self.updateVar(decl_var, .err, Rank.generalized); + }, + else => { + // Do nothing + }, + } } // 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, + }, +}; + +/// Given an annotation, generate the corresponding type based on the CIR +/// +/// This function will write the type into the type var node at `anno_idx` +fn generateAnnoTypeInPlace(self: *Self, anno_idx: CIR.TypeAnno.Idx, 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); + + // 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| { + try self.updateVar(anno_var, .{ .rigid = Rigid.init(rigid.name) }, Rank.generalized); + }, + .rigid_var_lookup => |rigid_lookup| { + try self.types.setVarRedirect(anno_var, ModuleEnv.varFrom(rigid_lookup.ref)); + }, + .underscore => { + try self.updateVar(anno_var, .{ .flex = Flex.init() }, Rank.generalized); + }, + .lookup => |lookup| { + switch (lookup.base) { + .builtin => |builtin_type| { + // TODO: Don't generate a new type var here, reuse anno var + const builtin_var = try self.generateBuiltinTypeInstance(lookup.name, builtin_type, &.{}, anno_region); + try self.types.setVarRedirect(anno_var, builtin_var); + }, + .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) { - 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); + // 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 = 0, + .num_actual_args = this_decl.num_args, + } }); + try self.updateVar(anno_var, .err, Rank.generalized); + return; + } + + // If so, then update this annotation to be an instance + // of this type using the same backing variable + try self.updateVar(anno_var, blk: { + switch (this_decl.type_) { + .alias => { + break :blk try self.types.mkAlias( + .{ .ident_idx = this_decl.name }, + this_decl.backing_var, + &.{}, + ); + }, + .nominal => { + break :blk try self.types.mkNominal( + .{ .ident_idx = this_decl.name }, + this_decl.backing_var, + &.{}, + self.common_idents.module_name, + ); + }, + } + }, Rank.generalized); + + return; + } + }, + .annotation => { + // Otherwise, we're in an annotation and this cannot + // be recursive + }, + } + + const instantiated_var = try self.instantiateVar( + ModuleEnv.varFrom(local.decl_idx), + Rank.generalized, + .{ .explicit = anno_region }, + ); + try self.types.setVarRedirect(anno_var, instantiated_var); + }, + .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, + Rank.generalized, + .{ .explicit = anno_region }, + ); + try self.types.setVarRedirect(anno_var, ext_instantiated_var); + } else { + // If this external type is unresolved, can should've reported + // an error. So we set to error and continue + try self.updateVar(anno_var, .err, Rank.generalized); + } + }, + } }, .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, 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| { + // TODO: Don't generate a new type var here, reuse anno var + const builtin_var = try self.generateBuiltinTypeInstance(a.name, builtin_type, anno_arg_vars, anno_region); + try self.types.setVarRedirect(anno_var, builtin_var); + }, + .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.updateVar(anno_var, .err, Rank.generalized); + return; + } + + // If so, then update this annotation to be an instance + // of this type using the same backing variable + try self.updateVar(anno_var, blk: { + switch (this_decl.type_) { + .alias => { + break :blk try self.types.mkAlias( + .{ .ident_idx = this_decl.name }, + this_decl.backing_var, + anno_arg_vars, + ); + }, + .nominal => { + break :blk try self.types.mkNominal( + .{ .ident_idx = this_decl.name }, + this_decl.backing_var, + anno_arg_vars, + self.common_idents.module_name, + ); + }, + } + }, Rank.generalized); + + 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.updateVar(anno_var, .err, Rank.generalized); + return; + } else { + std.debug.assert(false); + try self.updateVar(anno_var, .err, Rank.generalized); + 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.updateVar(anno_var, .err, Rank.generalized); + 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, + Rank.generalized, + .{ .explicit = anno_region }, + ); + try self.types.setVarRedirect(anno_var, instantiated_var); + }, + .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: { + if (ext_resolved == .alias) { + const decl_alias = ext_resolved.alias; + break :blk .{ self.types.sliceAliasArgs(decl_alias), decl_alias.ident.ident_idx }; + } else if (ext_resolved == .structure and ext_resolved.structure == .nominal_type) { + const decl_nominal = ext_resolved.structure.nominal_type; + break :blk .{ self.types.sliceNominalArgs(decl_nominal), decl_nominal.ident.ident_idx }; + } else if (ext_resolved == .err) { + try self.updateVar(anno_var, .err, Rank.generalized); + return; + } else { + std.debug.assert(false); + try self.updateVar(anno_var, .err, Rank.generalized); + 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.updateVar(anno_var, .err, Rank.generalized); + 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, + Rank.generalized, + .{ .explicit = anno_region }, + ); + try self.types.setVarRedirect(anno_var, instantiated_var); + } else { + // If this external type is unresolved, can should've reported + // an error. So we set to error and continue + try self.updateVar(anno_var, .err, Rank.generalized); + } + }, } }, .@"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, ctx); } - try self.checkAnnotation(func.ret); + const args_var_slice: []Var = @ptrCast(args_anno_slice); + + try self.generateAnnoTypeInPlace(func.ret, 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.updateVar(anno_var, fn_type, Rank.generalized); + }, + .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); + + // 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.updateVar(anno_var, .err, Rank.generalized); + 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, ctx); + } + const tag_vars_slice: []Var = @ptrCast(tag_anno_args_slice); + + // Add the processed tag to scratch + try self.scratch_tags.append(self.gpa, 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, ctx); + break :inner_blk ModuleEnv.varFrom(ext_anno_idx); + } else { + break :inner_blk try self.freshFromContent(.{ .structure = .empty_tag_union }, Rank.generalized, anno_region); + } + }; + + // Set the anno's type + try self.updateVar(anno_var, try self.types.mkTagUnion(tags_slice, ext_var), Rank.generalized); + }, + .tag => { + // This indicates a malformed type annotation. Tags should only + // exist as direct children of tag_unions + std.debug.assert(false); + try self.updateVar(anno_var, .err, Rank.generalized); + }, + .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, ctx); + const record_field_var = ModuleEnv.varFrom(rec_field.ty); + + // Add the processed tag to scratch + try self.scratch_record_fields.append(self.gpa, 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 means it's a closed union + // TODO: Capture ext in record field CIR + // const ext_var = inner_blk: { + // if (rec.ext) |ext_anno_idx| { + // try self.generateAnnoType(rigid_vars_ctx, ext_anno_idx); + // break :inner_blk ModuleEnv.varFrom(ext_anno_idx); + // } else { + // break :inner_blk try self.freshFromContent(.{ .structure = .empty_record }, Rank.generalized, anno_region); + // } + // }; + const ext_var = try self.freshFromContent(.{ .structure = .empty_record }, Rank.generalized, anno_region); + + // Create the type for the anno in the store + try self.updateVar( + anno_var, + .{ .structure = types_mod.FlatType{ .record = .{ + .fields = fields_type_range, + .ext = ext_var, + } } }, + Rank.generalized, + ); + }, + .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, ctx); + } + const elems_range = try self.types.appendVars(@ptrCast(elems_anno_slice)); + try self.updateVar(anno_var, .{ .structure = .{ .tuple = .{ .elems = elems_range } } }, Rank.generalized); }, .parens => |parens| { - try self.checkAnnotation(parens.anno); + try self.generateAnnoTypeInPlace(parens.anno, ctx); + try self.types.setVarRedirect(anno_var, ModuleEnv.varFrom(parens.anno)); + }, + .malformed => { + try self.updateVar(anno_var, .err, Rank.generalized); }, - else => {}, } } -/// Check and process a type application annotation (e.g., Maybe(x), List(String)). +/// Generate a type variable from the builtin /// -/// 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( +/// Writes the resulting type into the slot at `ret_var` +fn generateBuiltinTypeInstance( 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, -) std.mem.Allocator.Error!void { - // Clear any previous rigid variable mappings - self.annotation_rigid_var_subs.items.clearRetainingCapacity(); +) std.mem.Allocator.Error!Var { + switch (anno_builtin_type) { + .str => return try self.freshFromContent(.{ .structure = .str }, Rank.generalized, anno_region), + .u8 => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.int_u8 } }, Rank.generalized, anno_region), + .u16 => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.int_u16 } }, Rank.generalized, anno_region), + .u32 => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.int_u32 } }, Rank.generalized, anno_region), + .u64 => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.int_u64 } }, Rank.generalized, anno_region), + .u128 => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.int_u128 } }, Rank.generalized, anno_region), + .i8 => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.int_i8 } }, Rank.generalized, anno_region), + .i16 => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.int_i16 } }, Rank.generalized, anno_region), + .i32 => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.int_i32 } }, Rank.generalized, anno_region), + .i64 => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.int_i64 } }, Rank.generalized, anno_region), + .i128 => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.int_i128 } }, Rank.generalized, anno_region), + .f32 => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.frac_f32 } }, Rank.generalized, anno_region), + .f64 => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.frac_f64 } }, Rank.generalized, anno_region), + .dec => return try self.freshFromContent(.{ .structure = .{ .num = types_mod.Num.frac_dec } }, Rank.generalized, anno_region), + .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; + // Set error and return + return try self.freshFromContent(.err, Rank.generalized, anno_region); + } - 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); + // Create the type + return try self.freshFromContent(.{ .structure = .{ .list = anno_args[0] } }, Rank.generalized, anno_region); }, - .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 + .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 and return + return try self.freshFromContent(.err, Rank.generalized, anno_region); + } + + // Create the type + return try self.freshFromContent(.{ .structure = .{ .box = anno_args[0] } }, Rank.generalized, anno_region); }, - else => { - // Non-parameterized types don't need rigid variable mapping + .num => { + // 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 and return + return try self.freshFromContent(.err, Rank.generalized, anno_region); + } + + // Create the type + return try self.freshFromContent(.{ .structure = .{ + .num = .{ .num_poly = anno_args[0] }, + } }, Rank.generalized, anno_region); }, - } + .frac => { + // 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), + } }); - // 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 }, - ); + // Set error and return + return try self.freshFromContent(.err, Rank.generalized, anno_region); + } - // Redirect the annotation variable to point to the substituted type - try self.types.setVarRedirect(anno_var, instantiated_var); -} + // Create the type + const frac_var = try self.freshFromContent(.{ .structure = .{ .num = .{ + .frac_unbound = Num.FracRequirements.init(), + } } }, Rank.generalized, anno_region); + return try self.freshFromContent(.{ .structure = .{ .num = .{ + .num_poly = frac_var, + } } }, Rank.generalized, anno_region); + }, + .int => { + // 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), + } }); -/// 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; - } + // Set error and return + return try self.freshFromContent(.err, Rank.generalized, anno_region); + } - 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); - 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 type + const int_var = try self.freshFromContent(.{ .structure = .{ .num = .{ + .int_unbound = Num.IntRequirements.init(), + } } }, Rank.generalized, anno_region); + return try self.freshFromContent(.{ .structure = .{ .num = .{ + .num_poly = int_var, + } } }, Rank.generalized, anno_region); + }, } } // pattern // /// Check the types for the provided pattern -pub fn checkPattern(self: *Self, pattern_idx: CIR.Pattern.Idx) std.mem.Allocator.Error!void { +fn checkPattern(self: *Self, pattern_idx: CIR.Pattern.Idx, rank: types_mod.Rank, expected: Expected) std.mem.Allocator.Error!void { 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 = 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.updateVar(pattern_var, .{ .flex = Flex.init() }, rank); }, - .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.updateVar(pattern_var, .{ .flex = Flex.init() }, rank); }, - .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 => { + try self.updateVar(pattern_var, .{ .structure = .str }, rank); }, + // as // .as => |p| { - try self.checkPattern(p.pattern); + try self.checkPattern(p.pattern, rank, expected); }, - .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| { + // Check tuple elements + const elems_slice = self.cir.store.slicePatterns(tuple.patterns); + for (elems_slice) |single_elem_ptrn_idx| { + try self.checkPattern(single_elem_ptrn_idx, rank, .no_expectation); + } + + // 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.updateVar(pattern_var, .{ .structure = .{ + .tuple = .{ .elems = elem_vars_slice }, + } }, rank); + }, + // list // + .list => |list| { + const elems = self.cir.store.slicePatterns(list.patterns); + if (elems.len == 0) { + // If we have no elems, then set the type and move on + try self.updateVar(pattern_var, .{ .structure = .list_unbound }, rank); + } else { + // Here, we use the list's 1st element as the element var to + // constrain the rest of the list + + // Check the first elem + try self.checkPattern(elems[0], rank, .no_expectation); + + // Iterate over the remaining elements + const elem_var = ModuleEnv.varFrom(elems[0]); + var last_elem_ptrn_idx = elems[0]; + for (elems[1..], 1..) |elem_ptrn_idx, i| { + try self.checkPattern(elem_ptrn_idx, rank, .no_expectation); + const cur_elem_var = ModuleEnv.varFrom(elem_ptrn_idx); + + // Unify each element's var with the list's elem var + const result = try self.unify(elem_var, cur_elem_var, rank); + 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.checkPattern(remaining_elem_expr_idx, rank, .no_expectation); + } + + // Break to avoid cascading errors + break; + } + + last_elem_ptrn_idx = elem_ptrn_idx; + } + + // Now, set the type of the root variable t + try self.updateVar(pattern_var, .{ .structure = .{ .list = elem_var } }, rank); + + // 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| { + try self.checkPattern(rest_pattern_idx, rank, .no_expectation); + const rest_pattern_var = ModuleEnv.varFrom(rest_pattern_idx); + + _ = try self.unify(pattern_var, rest_pattern_var, rank); + } + } } }, - .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 + + // 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.checkPattern(arg_expr_idx, rank, .no_expectation); + } + + // Create the type + const ext_var = try self.fresh(rank, pattern_region); + try self.var_pool.addVarToRank(ext_var, rank); + + const tag = try self.types.mkTag(applied_tag.name, @ptrCast(arg_ptrn_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.updateVar(pattern_var, tag_union_content, rank); + }, + // nominal // + .nominal => |nominal| { + // TODO: Merge this with e_nominal_external + + // First, check the type inside the expr + try self.checkPattern(nominal.backing_pattern, rank, .no_expectation); + const actual_backing_var = ModuleEnv.varFrom(nominal.backing_pattern); + + // Then, we need an instance of the nominal type being referenced + // E.g. ConList.Cons(...) + // ^^^^^^^ + const nominal_var = try self.instantiateVar(ModuleEnv.varFrom(nominal.nominal_type_decl), rank, .{ .explicit = pattern_region }); + const nominal_resolved = self.types.resolveVar(nominal_var).desc.content; + + if (nominal_resolved == .structure and nominal_resolved.structure == .nominal_type) { + // Then, we extract the variable of the nominal type + // E.g. ConList(a) := [Cons(a, ConstList), Nil] + // ^^^^^^^^^^^^^^^^^^^^^^^^^ + const nominal_backing_var = self.types.getNominalBackingVar(nominal_resolved.structure.nominal_type); + + // Now we unify what the user wrote with the backing type of the nominal was + // E.g. ConList.Cons(...) <-> [Cons(a, ConsList(a)), Nil] + // ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ + const result = try self.unify(nominal_backing_var, actual_backing_var, rank); + + // Then, we handle the result of unification + switch (result) { + .ok => { + // If that unify call succeeded, then we this is a valid instance + // of this nominal type. So we set the expr's type to be the + // nominal type + try self.types.setVarRedirect(pattern_var, nominal_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 (nominal.backing_type) { + .tag => { + // Constructor doesn't exist or has wrong arity/types + self.setProblemTypeMismatchDetail(problem_idx, .invalid_nominal_tag); + }, + else => { + // TODO: Add specific error messages for records, tuples, etc. + }, + } + + // Mark the entire expression as having a type error + try self.updateVar(pattern_var, .err, rank); + }, + } + } else { + // If the nominal type is actually something else, then set the + // whole expression to be an error. + // + // TODO: Report a nice problem here + try self.updateVar(pattern_var, .err, rank); } }, - .record_destructure => |p| { - const destructs_slice = self.cir.store.sliceRecordDestructs(p.destructs); - for (destructs_slice) |destruct_idx| { + .nominal_external => |nominal| { + // TODO: Merge this with e_nominal + + // First, check the type inside the expr + try self.checkPattern(nominal.backing_pattern, rank, .no_expectation); + const actual_backing_var = ModuleEnv.varFrom(nominal.backing_pattern); + + if (try self.resolveVarFromExternal(nominal.module_idx, nominal.target_node_idx)) |ext_ref| { + // Then, we need an instance of the nominal type being referenced + // E.g. ConList.Cons(...) + // ^^^^^^^ + const nominal_var = try self.instantiateVar(ext_ref.local_var, Rank.generalized, .{ .explicit = pattern_region }); + const nominal_resolved = self.types.resolveVar(nominal_var).desc.content; + + if (nominal_resolved == .structure and nominal_resolved.structure == .nominal_type) { + // Then, we extract the variable of the nominal type + // E.g. ConList(a) := [Cons(a, ConstList), Nil] + // ^^^^^^^^^^^^^^^^^^^^^^^^^ + const nominal_backing_var = self.types.getNominalBackingVar(nominal_resolved.structure.nominal_type); + + // Now we unify what the user wrote with the backing type of the nominal was + // E.g. ConList.Cons(...) <-> [Cons(a, ConsList(a)), Nil] + // ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ + const result = try self.unify(nominal_backing_var, actual_backing_var, rank); + + // Then, we handle the result of unification + switch (result) { + .ok => { + // If that unify call succeeded, then we this is a valid instance + // of this nominal type. So we set the expr's type to be the + // nominal type + try self.types.setVarRedirect(pattern_var, nominal_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 (nominal.backing_type) { + .tag => { + // Constructor doesn't exist or has wrong arity/types + self.setProblemTypeMismatchDetail(problem_idx, .invalid_nominal_tag); + }, + else => { + // TODO: Add specific error messages for records, tuples, etc. + }, + } + + // Mark the entire expression as having a type error + try self.updateVar(pattern_var, .err, rank); + }, + } + } else { + // If the nominal type is actually something else, then set the + // whole expression to be an error. + // + // TODO: Report a nice problem here + try self.updateVar(pattern_var, .err, rank); + } + } else { + try self.updateVar(pattern_var, .err, rank); + } + }, + // 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); + + // Check the sub pattern + const field_pattern_idx = blk: { + switch (destruct.kind) { + .Required => |sub_pattern_idx| { + try self.checkPattern(sub_pattern_idx, rank, .no_expectation); + break :blk sub_pattern_idx; + }, + .SubPattern => |sub_pattern_idx| { + try self.checkPattern(sub_pattern_idx, rank, .no_expectation); + break :blk sub_pattern_idx; + }, + } + }; + const field_pattern_var = ModuleEnv.varFrom(field_pattern_idx); + + // Set the destruct var to redirect to the field pattern var + try self.types.setVarRedirect(destruct_var, field_pattern_var); + + // Append it to the scratch records array + try self.scratch_record_fields.append(self.gpa, 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.updateVar(pattern_var, .{ .structure = .{ + .record_unbound = record_fields_range, + } }, rank); + }, + // nums // + .num_literal => |num| { + const num_type = blk: { + switch (num.kind) { + .num_unbound => { + const int_reqs = num.value.toIntRequirements(); + const frac_reqs = num.value.toFracRequirements(); + break :blk Num{ .num_unbound = .{ .int_requirements = int_reqs, .frac_requirements = frac_reqs } }; + }, + .int_unbound => { + const int_reqs = num.value.toIntRequirements(); + const int_var = try self.freshFromContent(.{ .structure = .{ .num = .{ .int_unbound = int_reqs } } }, rank, pattern_region); + try self.var_pool.addVarToRank(int_var, rank); + break :blk Num{ .num_poly = int_var }; + }, + .u8 => break :blk Num{ .num_compact = Num.Compact{ .int = .u8 } }, + .i8 => break :blk Num{ .num_compact = Num.Compact{ .int = .i8 } }, + .u16 => break :blk Num{ .num_compact = Num.Compact{ .int = .u16 } }, + .i16 => break :blk Num{ .num_compact = Num.Compact{ .int = .i16 } }, + .u32 => break :blk Num{ .num_compact = Num.Compact{ .int = .u32 } }, + .i32 => break :blk Num{ .num_compact = Num.Compact{ .int = .i32 } }, + .u64 => break :blk Num{ .num_compact = Num.Compact{ .int = .u64 } }, + .i64 => break :blk Num{ .num_compact = Num.Compact{ .int = .i64 } }, + .u128 => break :blk Num{ .num_compact = Num.Compact{ .int = .u128 } }, + .i128 => break :blk Num{ .num_compact = Num.Compact{ .int = .i128 } }, + .f32 => break :blk Num{ .num_compact = Num.Compact{ .frac = .f32 } }, + .f64 => break :blk Num{ .num_compact = Num.Compact{ .frac = .f64 } }, + .dec => break :blk Num{ .num_compact = Num.Compact{ .frac = .dec } }, + } + }; + + // Update the pattern var + try self.updateVar(pattern_var, .{ .structure = .{ .num = num_type } }, rank); + }, + .frac_f32_literal => |_| { + try self.updateVar(pattern_var, .{ .structure = .{ .num = .{ .num_compact = .{ .frac = .f32 } } } }, rank); + }, + .frac_f64_literal => |_| { + try self.updateVar(pattern_var, .{ .structure = .{ .num = .{ .num_compact = .{ .frac = .f64 } } } }, rank); + }, + .dec_literal => |dec| { + if (dec.has_suffix) { + try self.updateVar(pattern_var, .{ .structure = .{ .num = .{ .num_compact = .{ .frac = .dec } } } }, rank); + } else { + const f64_val = dec.value.toF64(); + const requirements = types_mod.Num.FracRequirements{ + .fits_in_f32 = can.CIR.fitsInF32(f64_val), + .fits_in_dec = can.CIR.fitsInDec(f64_val), + }; + const frac_var = try self.freshFromContent(.{ .structure = .{ .num = .{ + .frac_unbound = requirements, + } } }, rank, pattern_region); + try self.var_pool.addVarToRank(frac_var, rank); + + try self.updateVar(pattern_var, .{ .structure = .{ .num = .{ + .num_poly = frac_var, + } } }, rank); + } + }, + .small_dec_literal => |dec| { + if (dec.has_suffix) { + try self.updateVar(pattern_var, .{ .structure = .{ .num = .{ .num_compact = .{ .frac = .dec } } } }, rank); + } else { + const reqs = dec.value.toFracRequirements(); + const frac_var = try self.freshFromContent(.{ .structure = .{ .num = .{ + .frac_unbound = reqs, + } } }, rank, pattern_region); + try self.var_pool.addVarToRank(frac_var, rank); + + try self.updateVar(pattern_var, .{ .structure = .{ .num = .{ + .num_poly = frac_var, + } } }, rank); + } + }, + .runtime_error => { + try self.updateVar(pattern_var, .err, rank); + }, + } + + // 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.unifyFromAnno(expected_type.var_, pattern_var, rank); + } else { + _ = try self.unify(expected_type.var_, pattern_var, rank); } }, - else => {}, } } // 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); -} +const Expected = union(enum) { + no_expectation, + expected: struct { var_: Var, from_annotation: bool }, +}; -/// 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, rank: types_mod.Rank, expected: Expected) std.mem.Allocator.Error!bool { const trace = tracy.trace(@src()); defer trace.end(); + std.debug.assert(rank == self.var_pool.current_rank); + const expr = self.cir.store.getExpr(expr_idx); const expr_var = ModuleEnv.varFrom(expr_idx); const expr_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(expr_idx)); 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. + // str // + .e_str_segment => |_| { + try self.updateVar(expr_var, .{ .structure = .str }, rank); + }, + .e_str => |str| { + // Iterate over the string segments, capturing if any error'd + const segment_expr_idx_slice = self.cir.store.sliceExpr(str.span); + var did_err = false; + for (segment_expr_idx_slice) |seg_expr_idx| { + // Check the segment + does_fx = try self.checkExpr(seg_expr_idx, rank, .no_expectation) or does_fx; - // 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); - } + // Check if it errored + const seg_var = ModuleEnv.varFrom(seg_expr_idx); + did_err = did_err or self.types.resolveVar(seg_var).desc.content == .err; } - }, - .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))); - _ = 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); - } + if (did_err) { + // If any segment errored, propgate that error to the root string + try self.updateVar(expr_var, .err, rank); } else { - // Import not found - try self.types.setVarContent(expr_var, .err); + // Otherwise, set the type of this expr to be string + try self.updateVar(expr_var, .{ .structure = .str }, rank); } }, + // nums // + .e_num => |num| { + const num_type = blk: { + switch (num.kind) { + .num_unbound => { + const int_reqs = num.value.toIntRequirements(); + const frac_reqs = num.value.toFracRequirements(); + break :blk Num{ .num_unbound = .{ .int_requirements = int_reqs, .frac_requirements = frac_reqs } }; + }, + .int_unbound => { + const int_reqs = num.value.toIntRequirements(); + const int_var = try self.freshFromContent(.{ .structure = .{ .num = .{ .int_unbound = int_reqs } } }, rank, expr_region); + try self.var_pool.addVarToRank(int_var, rank); + break :blk Num{ .num_poly = int_var }; + }, + .u8 => break :blk Num{ .num_compact = Num.Compact{ .int = .u8 } }, + .i8 => break :blk Num{ .num_compact = Num.Compact{ .int = .i8 } }, + .u16 => break :blk Num{ .num_compact = Num.Compact{ .int = .u16 } }, + .i16 => break :blk Num{ .num_compact = Num.Compact{ .int = .i16 } }, + .u32 => break :blk Num{ .num_compact = Num.Compact{ .int = .u32 } }, + .i32 => break :blk Num{ .num_compact = Num.Compact{ .int = .i32 } }, + .u64 => break :blk Num{ .num_compact = Num.Compact{ .int = .u64 } }, + .i64 => break :blk Num{ .num_compact = Num.Compact{ .int = .i64 } }, + .u128 => break :blk Num{ .num_compact = Num.Compact{ .int = .u128 } }, + .i128 => break :blk Num{ .num_compact = Num.Compact{ .int = .i128 } }, + .f32 => break :blk Num{ .num_compact = Num.Compact{ .frac = .f32 } }, + .f64 => break :blk Num{ .num_compact = Num.Compact{ .frac = .f64 } }, + .dec => break :blk Num{ .num_compact = Num.Compact{ .frac = .dec } }, + } + }; + + // Update the pattern var + try self.updateVar(expr_var, .{ .structure = .{ .num = num_type } }, rank); + }, + .e_frac_f32 => |frac| { + if (frac.has_suffix) { + try self.updateVar(expr_var, .{ .structure = .{ .num = .{ .num_compact = .{ .frac = .f32 } } } }, rank); + } else { + const requirements = types_mod.Num.FracRequirements{ + .fits_in_f32 = true, + .fits_in_dec = can.CIR.fitsInDec(@floatCast(frac.value)), + }; + const frac_var = try self.freshFromContent(.{ .structure = .{ .num = .{ + .frac_unbound = requirements, + } } }, rank, expr_region); + try self.var_pool.addVarToRank(frac_var, rank); + + try self.updateVar(expr_var, .{ .structure = .{ .num = .{ + .num_poly = frac_var, + } } }, rank); + } + }, + .e_frac_f64 => |frac| { + if (frac.has_suffix) { + try self.updateVar(expr_var, .{ .structure = .{ .num = .{ .num_compact = .{ .frac = .f64 } } } }, rank); + } else { + const requirements = types_mod.Num.FracRequirements{ + .fits_in_f32 = can.CIR.fitsInF32(@floatCast(frac.value)), + .fits_in_dec = can.CIR.fitsInDec(@floatCast(frac.value)), + }; + const frac_var = try self.freshFromContent(.{ .structure = .{ .num = .{ + .frac_unbound = requirements, + } } }, rank, expr_region); + try self.var_pool.addVarToRank(frac_var, rank); + + try self.updateVar(expr_var, .{ .structure = .{ .num = .{ + .num_poly = frac_var, + } } }, rank); + } + }, + .e_dec => |frac| { + if (frac.has_suffix) { + try self.updateVar(expr_var, .{ .structure = .{ .num = .{ .num_compact = .{ .frac = .dec } } } }, rank); + } else { + const f64_val = frac.value.toF64(); + const requirements = types_mod.Num.FracRequirements{ + .fits_in_f32 = can.CIR.fitsInF32(f64_val), + .fits_in_dec = can.CIR.fitsInDec(f64_val), + }; + const frac_var = try self.freshFromContent(.{ .structure = .{ .num = .{ + .frac_unbound = requirements, + } } }, rank, expr_region); + try self.var_pool.addVarToRank(frac_var, rank); + + try self.updateVar(expr_var, .{ .structure = .{ .num = .{ + .num_poly = frac_var, + } } }, rank); + } + }, + .e_dec_small => |frac| { + if (frac.has_suffix) { + try self.updateVar(expr_var, .{ .structure = .{ .num = .{ .num_compact = .{ .frac = .dec } } } }, rank); + } else { + const reqs = frac.value.toFracRequirements(); + const frac_var = try self.freshFromContent(.{ .structure = .{ .num = .{ + .frac_unbound = reqs, + } } }, rank, expr_region); + try self.var_pool.addVarToRank(frac_var, rank); + + try self.updateVar(expr_var, .{ .structure = .{ .num = .{ + .num_poly = frac_var, + } } }, rank); + } + }, + // list // + .e_empty_list => { + try self.updateVar(expr_var, .{ .structure = .list_unbound }, rank); + }, .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 + if (elems.len == 0) { + // If we have no elems, then set the type and move on + try self.updateVar(expr_var, .{ .structure = .list_unbound }, rank); + } else { + // Here, we use the list's 1st element as the element var to + // constrain the rest of the 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; + // Check the first elem + does_fx = try self.checkExpr(elems[0], rank, .no_expectation) or does_fx; - for (elems[1..], 1..) |elem_expr_id, i| { - does_fx = try self.checkExpr(elem_expr_id) 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, rank, .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, @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), - } }); + // Unify each element's var with the list's elem var + const result = try self.unify(elem_var, cur_elem_var, rank); + 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 (!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; + // 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, rank, .no_expectation) or does_fx; + } + + // Break to avoid cascading errors + break; } - // Break to avoid cascading errors - break; + last_elem_expr_idx = elem_expr_idx; } - last_elem_idx = elem_expr_id; + try self.updateVar(expr_var, .{ .structure = .{ .list = elem_var } }, rank); } }, - .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; + // 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, rank, .no_expectation) 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)); + // 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)); - 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; - }, - else => { - // Non-structure content - fall through - }, - } - - // 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 = @ptrCast(@alignCast(@constCast(call_args))); - - // 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); - } + // Set the type in the store + try self.updateVar(expr_var, .{ .structure = .{ + .tuple = .{ .elems = elem_vars_slice }, + } }, rank); }, + // record // .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 + // Create a record type in the type system and assign it the expr_var - const record_var_resolved = self.types.resolveVar(expr_var); - const record_var_content = record_var_resolved.desc.content; + // 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, rank, .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(self.gpa, 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; - } - } - } - // If record_var_content is NOT .structure.record, unification is skipped - // This typically happens when canonicalization didn't set the record structure properly + // Check if we have an ext + if (e.ext) |ext_expr| { + does_fx = try self.checkExpr(ext_expr, rank, .no_expectation) or does_fx; + try self.updateVar(expr_var, .{ .structure = .{ .record = .{ + .ext = ModuleEnv.varFrom(ext_expr), + .fields = record_fields_range, + } } }, rank); + } else { + try self.updateVar(expr_var, .{ .structure = .{ + .record_unbound = record_fields_range, + } }, rank); } }, - .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); + .e_empty_record => { + try self.updateVar(expr_var, .{ .structure = .empty_record }, rank); + }, + // tags // + .e_zero_argument_tag => |e| { + const ext_var = try self.fresh(rank, expr_region); + try self.var_pool.addVarToRank(ext_var, rank); - try self.checkNominal( - ModuleEnv.varFrom(expr_idx), - expr_region, - expr_backing_var, - e.backing_type, - real_nominal_var, - ); + 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.updateVar(expr_var, tag_union_content, rank); }, - .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_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, rank, .no_expectation) or does_fx; + } + + // Create the type + const ext_var = try self.fresh(rank, expr_region); + try self.var_pool.addVarToRank(ext_var, rank); + + 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.updateVar(expr_var, tag_union_content, rank); }, - .e_zero_argument_tag => |_| {}, - .e_binop => |binop| { - does_fx = try self.checkBinopExpr(expr_idx, expr_region, binop, expected_type, from_annotation); + // nominal // + .e_nominal => |nominal| { + // TODO: Merge this with e_nominal_external + + // First, check the type inside the expr + does_fx = try self.checkExpr(nominal.backing_expr, rank, .no_expectation) or does_fx; + const actual_backing_var = ModuleEnv.varFrom(nominal.backing_expr); + + // Then, we need an instance of the nominal type being referenced + // E.g. ConList.Cons(...) + // ^^^^^^^ + const nominal_var = try self.instantiateVar(ModuleEnv.varFrom(nominal.nominal_type_decl), rank, .{ .explicit = expr_region }); + const nominal_resolved = self.types.resolveVar(nominal_var).desc.content; + + if (nominal_resolved == .structure and nominal_resolved.structure == .nominal_type) { + // Then, we extract the variable of the nominal type + // E.g. ConList(a) := [Cons(a, ConstList), Nil] + // ^^^^^^^^^^^^^^^^^^^^^^^^^ + const nominal_backing_var = self.types.getNominalBackingVar(nominal_resolved.structure.nominal_type); + + // Now we unify what the user wrote with the backing type of the nominal was + // E.g. ConList.Cons(...) <-> [Cons(a, ConsList(a)), Nil] + // ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ + const result = try self.unify(nominal_backing_var, actual_backing_var, rank); + + // Then, we handle the result of unification + switch (result) { + .ok => { + // If that unify call succeeded, then we this is a valid instance + // of this nominal type. So we set the expr's type to be the + // nominal type + try self.types.setVarRedirect(expr_var, nominal_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 (nominal.backing_type) { + .tag => { + // Constructor doesn't exist or has wrong arity/types + self.setProblemTypeMismatchDetail(problem_idx, .invalid_nominal_tag); + }, + else => { + // TODO: Add specific error messages for records, tuples, etc. + }, + } + + // Mark the entire expression as having a type error + try self.updateVar(expr_var, .err, rank); + }, + } + } else { + // If the nominal type is actually something else, then set the + // whole expression to be an error. + // + // TODO: Report a nice problem here + try self.updateVar(expr_var, .err, rank); + } }, - .e_unary_minus => |unary| { - does_fx = try self.checkUnaryMinusExpr(expr_idx, expr_region, unary); + .e_nominal_external => |nominal| { + // TODO: Merge this with e_nominal + + // First, check the type inside the expr + does_fx = try self.checkExpr(nominal.backing_expr, rank, .no_expectation) or does_fx; + const actual_backing_var = ModuleEnv.varFrom(nominal.backing_expr); + + if (try self.resolveVarFromExternal(nominal.module_idx, nominal.target_node_idx)) |ext_ref| { + // Then, we need an instance of the nominal type being referenced + // E.g. ConList.Cons(...) + // ^^^^^^^ + const nominal_var = try self.instantiateVar(ext_ref.local_var, Rank.generalized, .{ .explicit = expr_region }); + const nominal_resolved = self.types.resolveVar(nominal_var).desc.content; + + if (nominal_resolved == .structure and nominal_resolved.structure == .nominal_type) { + // Then, we extract the variable of the nominal type + // E.g. ConList(a) := [Cons(a, ConstList), Nil] + // ^^^^^^^^^^^^^^^^^^^^^^^^^ + const nominal_backing_var = self.types.getNominalBackingVar(nominal_resolved.structure.nominal_type); + + // Now we unify what the user wrote with the backing type of the nominal was + // E.g. ConList.Cons(...) <-> [Cons(a, ConsList(a)), Nil] + // ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ + const result = try self.unify(nominal_backing_var, actual_backing_var, rank); + + // Then, we handle the result of unification + switch (result) { + .ok => { + // If that unify call succeeded, then we this is a valid instance + // of this nominal type. So we set the expr's type to be the + // nominal type + try self.types.setVarRedirect(expr_var, nominal_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 (nominal.backing_type) { + .tag => { + // Constructor doesn't exist or has wrong arity/types + self.setProblemTypeMismatchDetail(problem_idx, .invalid_nominal_tag); + }, + else => { + // TODO: Add specific error messages for records, tuples, etc. + }, + } + + // Mark the entire expression as having a type error + try self.updateVar(expr_var, .err, rank); + }, + } + } else { + // If the nominal type is actually something else, then set the + // whole expression to be an error. + // + // TODO: Report a nice problem here + try self.updateVar(expr_var, .err, rank); + } + } else { + try self.updateVar(expr_var, .err, rank); + } }, - .e_unary_not => |unary| { - does_fx = try self.checkUnaryNotExpr(expr_idx, expr_region, unary); + // lookup // + .e_lookup_local => |lookup| { + const pat_var = ModuleEnv.varFrom(lookup.pattern_idx); + const resolved_pat = self.types.resolveVar(pat_var).desc; + + // We never instantiate rigid variables + if (resolved_pat.rank == Rank.generalized and resolved_pat.content != .rigid) { + const instantiated = try self.instantiateVar(pat_var, rank, .use_last_var); + _ = try self.types.setVarRedirect(expr_var, instantiated); + } else { + _ = try self.types.setVarRedirect(expr_var, pat_var); + } + + // 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, + Rank.generalized, + .{ .explicit = expr_region }, + ); + try self.types.setVarRedirect(expr_var, ext_instantiated_var); + } else { + try self.updateVar(expr_var, .err, rank); + } + }, + // 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); + // Check the pattern + try self.checkPattern(decl_stmt.pattern, rank, .no_expectation); + const decl_pattern_var: Var = ModuleEnv.varFrom(decl_stmt.pattern); - 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; + // Check the annotation, if it exists + const check_mode = blk: { + if (decl_stmt.anno) |anno_idx| { + const annotation = self.cir.store.getAnnotation(anno_idx); + try self.generateAnnoTypeInPlace(annotation.type_anno, .annotation); + const anno_var = ModuleEnv.varFrom(anno_idx); + break :blk Expected{ + .expected = .{ .var_ = anno_var, .from_annotation = true }, + }; + } else { + break :blk Expected.no_expectation; + } + }; + + { + // Enter a new rank + try self.var_pool.pushRank(); + defer self.var_pool.popRank(); + + const next_rank = rank.next(); + std.debug.assert(next_rank == self.var_pool.current_rank); + + does_fx = try self.checkExpr(decl_stmt.expr, next_rank, check_mode) or does_fx; + + // Now that we are existing the scope, we must generalize then pop this rank + try self.generalizer.generalize(&self.var_pool, next_rank); } // 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); + const decl_expr_var: Var = ModuleEnv.varFrom(decl_stmt.expr); + _ = try self.unify(decl_pattern_var, decl_expr_var, rank); }, .s_reassign => |reassign| { - does_fx = try self.checkExpr(reassign.expr) or does_fx; + // Check the pattern + try self.checkPattern(reassign.pattern_idx, rank, .no_expectation); + const reassign_pattern_var: Var = ModuleEnv.varFrom(reassign.pattern_idx); + + { + // Enter a new rank + try self.var_pool.pushRank(); + defer self.var_pool.popRank(); + + const next_rank = rank.next(); + std.debug.assert(next_rank == self.var_pool.current_rank); + + does_fx = try self.checkExpr(reassign.expr, next_rank, .no_expectation) or does_fx; + + // Now that we are existing the scope, we must generalize then pop this rank + try self.generalizer.generalize(&self.var_pool, next_rank); + } + + // Unify the pattern with the expression + const reassign_expr_var: Var = ModuleEnv.varFrom(reassign.expr); + _ = try self.unify(reassign_pattern_var, reassign_expr_var, rank); }, .s_expr => |expr_stmt| { - does_fx = try self.checkExpr(expr_stmt.expr) or does_fx; + does_fx = try self.checkExpr(expr_stmt.expr, rank, .no_expectation) or does_fx; + }, + .s_expect => |expr_stmt| { + does_fx = try self.checkExpr(expr_stmt.body, rank, .no_expectation) or does_fx; + const stmt_expr: Var = ModuleEnv.varFrom(expr_stmt.body); + + const bool_var = try self.freshBool(rank, expr_region); + _ = try self.unify(bool_var, stmt_expr, rank); }, else => { - // Other statement types don't need expression checking + // TODO }, } } // Check the final expression - does_fx = try self.checkExpr(block.final_expr) or does_fx; + does_fx = try self.checkExpr(block.final_expr, rank, 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); - } else { - does_fx = try self.checkExpr(closure.lambda_idx); - } - const lambda_var = ModuleEnv.varFrom(closure.lambda_idx); - const closure_var = ModuleEnv.varFrom(expr_idx); - _ = try self.unify(closure_var, lambda_var); + try self.types.setVarRedirect(expr_var, ModuleEnv.varFrom(block.final_expr)); }, + // 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; + // 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 }; + }, + } + }; + + // 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; + while (true) { + 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, + } + }, + .alias => |alias| { + var_ = self.types.getAliasBackingVar(alias); + }, + else => break :blk null, + } + } + } else { + break :blk null; + } + }; + + // Enter the next rank + try self.var_pool.pushRank(); + defer self.var_pool.popRank(); + + const next_rank = rank.next(); + std.debug.assert(next_rank == self.var_pool.current_rank); + + // 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, next_rank, .no_expectation); } - // The tuple type is created in the type store in canonicalize, so - // nothing more needs to be done here + // 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, rank); + 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 = null, // TODO: Use function 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.updateVar(expr_var, .err, rank); + 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.unifyFromAnno(expected_arg_var, ModuleEnv.varFrom(pattern_idx), next_rank); + } else { + _ = try self.unify(expected_arg_var, ModuleEnv.varFrom(pattern_idx), next_rank); + } + } + } 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); + + // Check the the body of the expr + // If we have an expected function, use that as the expr's expected type + if (mb_expected_func) |expected_func| { + does_fx = try self.checkExpr(lambda.body, next_rank, .{ + .expected = .{ .var_ = expected_func.ret, .from_annotation = is_expected_from_anno }, + }) or does_fx; + } else { + does_fx = try self.checkExpr(lambda.body, next_rank, .no_expectation) or does_fx; + } + const body_var = ModuleEnv.varFrom(lambda.body); + + // Create the function type + if (does_fx) { + _ = try self.updateVar(expr_var, try self.types.mkFuncEffectful(arg_vars, body_var), next_rank); + } else { + _ = try self.updateVar(expr_var, try self.types.mkFuncUnbound(arg_vars, body_var), next_rank); + } + try self.var_pool.addVarToRank(expr_var, next_rank); + + // 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 + + // Now that we are existing the scope, we must generalize then pop this rank + try self.generalizer.generalize(&self.var_pool, next_rank); + }, + .e_closure => |closure| { + does_fx = try self.checkExpr(closure.lambda_idx, rank, expected) or does_fx; + _ = try self.types.setVarRedirect(expr_var, ModuleEnv.varFrom(closure.lambda_idx)); + }, + // 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, rank, .no_expectation) or does_fx; + const func_var = ModuleEnv.varFrom(call.func); + + // 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, rank, .no_expectation) or does_fx; + } + + // From the base function type, extract the actual function info + const mb_func: ?types_mod.Func = inner_blk: { + // Here, we unwrap the function, following aliases, to get + // the actual function we want to check against + var var_ = func_var; + while (true) { + switch (self.types.resolveVar(var_).desc.content) { + .structure => |flat_type| { + switch (flat_type) { + .fn_pure => |func| break :inner_blk func, + .fn_unbound => |func| break :inner_blk func, + .fn_effectful => |func| break :inner_blk func, + else => break :inner_blk null, + } + }, + .alias => |alias| { + var_ = self.types.getAliasBackingVar(alias); + }, + else => break :inner_blk null, + } + } + }; + + // 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, + } + }; + + // Now, check the call args against the type of function + if (mb_func) |func| { + const func_args = self.types.sliceVars(func.args); + + 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); + + // 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); + + // Skip any concrete arguments + if (expected_resolved_1.desc.content != .flex) { + continue; + } + + // 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* + + const arg_1 = @as(Var, ModuleEnv.varFrom(call_arg_expr_idxs[i])); + const arg_2 = @as(Var, ModuleEnv.varFrom(call_arg_expr_idxs[j])); + + const unify_result = try self.unify(arg_1, arg_2, rank); + 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), + }, + }); + + // Stop execution + _ = try self.updateVar(expr_var, .err, rank); + break :blk; + } + } + } + } + + // 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), rank); + 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), + }, + }); + + // Stop execution + _ = try self.updateVar(expr_var, .err, rank); + break :blk; + } + } + + // Redirect the expr to the function's return type + _ = try self.types.setVarRedirect(expr_var, func.ret); + } else { + // TODO(jared): Better arity difference error message + + // If the expected function's arity doesn't match + // the actual arguments provoided, unify the + // inferred function type with the expected function + // type to get the regulare error message + const call_arg_vars: []Var = @ptrCast(call_arg_expr_idxs); + const call_func_ret = try self.fresh(rank, 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, rank, expr_region); + + try self.var_pool.addVarToRank(call_func_ret, rank); + try self.var_pool.addVarToRank(call_func_var, rank); + + _ = try self.unify(func_var, call_func_var, rank); + _ = try self.types.setVarRedirect(expr_var, call_func_ret); + } + } 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. + + // 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) + + // 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 + + const call_arg_vars: []Var = @ptrCast(call_arg_expr_idxs); + const call_func_ret = try self.fresh(rank, 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, rank, expr_region); + + try self.var_pool.addVarToRank(call_func_ret, rank); + try self.var_pool.addVarToRank(call_func_var, rank); + + _ = try self.unify(func_var, call_func_var, rank); + + // 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.types.setVarRedirect(expr_var, call_func_ret); + } + }, + else => { + // No other call types are currently supported in czer + std.debug.assert(false); + try self.updateVar(expr_var, .err, rank); + }, + } + }, + .e_if => |if_expr| { + does_fx = try self.checkIfElseExpr(expr_idx, expr_region, rank, if_expr) or does_fx; + }, + .e_match => |match| { + does_fx = try self.checkMatchExpr(expr_idx, rank, match) or does_fx; + }, + .e_binop => |binop| { + does_fx = try self.checkBinopExpr(expr_idx, expr_region, rank, binop, expected) or does_fx; + }, + .e_unary_minus => |unary| { + does_fx = try self.checkUnaryMinusExpr(expr_idx, expr_region, rank, unary) or does_fx; + }, + .e_unary_not => |unary| { + does_fx = try self.checkUnaryNotExpr(expr_idx, expr_region, rank, unary) or does_fx; }, .e_dot_access => |dot_access| { // Check the receiver expression - does_fx = try self.checkExpr(dot_access.receiver) or does_fx; + does_fx = try self.checkExpr(dot_access.receiver, rank, .no_expectation) 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); + const receiver_var = ModuleEnv.varFrom(dot_access.receiver); + + if (dot_access.args) |args_span| { + // If this dot access has args, then it's static dispatch + + const resolved_receiver = self.types.resolveVar(receiver_var); + switch (resolved_receiver.desc.content) { + .err => { + // If the receiver type is an error, then propgate it and break + try self.updateVar(expr_var, .err, rank); + }, + .structure => |structure| switch (structure) { + .nominal_type => |nominal| { + // This is a static dispatch on a nominal type - // 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); @@ -1323,46 +2658,35 @@ fn checkExprWithExpectedAndAnnotationHelp(self: *Self, expr_idx: CIR.Expr.Idx, e 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 source_var = ModuleEnv.varFrom(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); + const method_instantiated = try self.instantiateVar(method_var, rank, .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.array_list.Managed(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); + const arg_expr_idxs = self.cir.store.sliceExpr(args_span); + for (arg_expr_idxs) |arg_expr_idx| { + does_fx = try self.checkExpr(arg_expr_idx, rank, .no_expectation) or does_fx; } + const arg_expr_vars: []Var = @ptrCast(arg_expr_idxs); // 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); + const ret_var = try self.fresh(rank, expr_region); + const func_content = try self.types.mkFuncUnbound(arg_expr_vars, ret_var); + const expected_func_var = try self.freshFromContent(func_content, rank, expr_region); // Unify with the imported method type - _ = try self.unify(expected_func_var, method_instantiated); + const result = try self.unify(method_instantiated, expected_func_var, rank); - // 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 + // If the inferred function type matches the referenced type + // then set the expr type to be the return type + if (result.isOk()) { + try self.types.setVarRedirect(expr_var, ret_var); + } else { + try self.updateVar(expr_var, .err, rank); + } } else { // Method not found in origin module // TODO: Add a proper error type for method not found on nominal type @@ -1373,683 +2697,69 @@ fn checkExprWithExpectedAndAnnotationHelp(self: *Self, expr_idx: CIR.Expr.Idx, e // 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); - } - }, - .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 nominal var, this is a type error + // TODO: Add a proper error when receiver is not a nominal var + }, }, else => { - // Receiver is not a record, this is a type error - // For now, we'll let unification handle the error + // Receiver is not a nominal var, this is a type error + // TODO: Add a proper error when receiver is not a nominal var + }, - }, - .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), - }, - }); - return false; // Early return on error - } - } - } - } - - // 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 = @ptrCast(@alignCast(@constCast(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: @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: @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); - } else { - _ = try self.unify(lhs_var, expected); - _ = try self.unify(rhs_var, expected); - _ = try self.unify(result_var, expected); } } 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); + // Otherwise, this is dot access on a record - // 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); + // 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(rank, 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, + } }, rank, expr_region); + + // Then, unify the actual receiver type with the expected record + _ = try self.unify(record_being_accessed, receiver_var, rank); + try self.types.setVarRedirect(expr_var, record_field_var); } - - return does_fx; }, - .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_crash => { + try self.updateVar(expr_var, .{ .flex = Flex.init() }, rank); }, - .@"and" => { - var does_fx = try self.checkExpr(binop.lhs); - does_fx = try self.checkExpr(binop.rhs) or does_fx; - - 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", - } }); - - 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", - } }); - } - - return does_fx; + .e_dbg => |dbg| { + does_fx = try self.checkExpr(dbg.expr, rank, expected) or does_fx; + _ = try self.types.setVarRedirect(expr_var, ModuleEnv.varFrom(dbg.expr)); }, - .@"or" => { - var does_fx = try self.checkExpr(binop.lhs); - does_fx = try self.checkExpr(binop.rhs) or does_fx; - - 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 (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", - } }); - } - - return does_fx; + .e_expect => |expect| { + does_fx = try self.checkExpr(expect.body, rank, expected) or does_fx; + try self.updateVar(expr_var, .{ .structure = .empty_record }, rank); }, - .pipe_forward => { - var does_fx = try self.checkExpr(binop.lhs); - does_fx = try self.checkExpr(binop.rhs) or does_fx; - return does_fx; + .e_ellipsis => { + try self.updateVar(expr_var, .{ .flex = Flex.init() }, rank); }, - .null_coalesce => { - var does_fx = try self.checkExpr(binop.lhs); - does_fx = try self.checkExpr(binop.rhs) or does_fx; - return does_fx; + .e_runtime_error => { + try self.updateVar(expr_var, .err, rank); }, } -} -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(); - - // Check the operand expression - const does_fx = try self.checkExpr(unary.expr); - - // 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))); - - // 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); - - // Unify operand and result with the number type - _ = try self.unify(operand_var, num_var); - _ = try self.unify(result_var, num_var); - - return does_fx; -} - -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(); - - // Check the operand expression - const does_fx = try self.checkExpr(unary.expr); - - // 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))); - - // Create a fresh boolean variable for the operation - const bool_var = try self.instantiateVarAnon(ModuleEnv.varFrom(can.Can.BUILTIN_BOOL_TYPE), .{ .explicit = expr_region }); - - // Unify operand and result with the boolean type - _ = try self.unify(operand_var, bool_var); - _ = try self.unify(result_var, bool_var); - - return does_fx; -} - -// nominal // - -// nominal // - -/// 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(); - - // 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. - - // 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); - }, - else => { - // TODO: Add specific error messages for records, tuples, etc. - }, + // 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.unifyFromAnno(expected_type.var_, expr_var, rank); + } else { + _ = try self.unify(expected_type.var_, expr_var, rank); } - - // Mark the entire expression as having a type error - try self.types.setVarContent(node_var, .err); }, } + + return does_fx; } // if-else // @@ -2059,7 +2769,8 @@ fn checkIfElseExpr( self: *Self, if_expr_idx: CIR.Expr.Idx, expr_region: Region, - if_: @FieldType(CIR.Expr, "e_if"), + rank: types_mod.Rank, + if_: std.meta.FieldType(CIR.Expr, .e_if), ) std.mem.Allocator.Error!bool { const trace = tracy.trace(@src()); defer trace.end(); @@ -2074,36 +2785,38 @@ 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, rank, .no_expectation); + const first_cond_var: Var = ModuleEnv.varFrom(first_branch.cond); + const bool_var = try self.freshBool(rank, expr_region); + const first_cond_result = try self.unify(bool_var, first_cond_var, rank); 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, rank, .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); var last_if_branch = first_branch_idx; for (branches[1..], 1..) |branch_idx, cur_index| { + // TODO: Each branch body get it's own rank?? + 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, rank, .no_expectation) or does_fx; + const cond_var: Var = ModuleEnv.varFrom(branch.cond); + const branch_bool_var = try self.freshBool(rank, expr_region); + const cond_result = try self.unify(branch_bool_var, cond_var, rank); 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, rank, .no_expectation) or does_fx; + const body_var: Var = ModuleEnv.varFrom(branch.body); + const body_result = try self.unify(branch_var, body_var, rank); self.setDetailIfTypeMismatch(body_result, problem.TypeMismatchDetail{ .incompatible_if_branches = .{ .parent_if_expr = if_expr_idx, .last_if_branch = last_if_branch, @@ -2116,15 +2829,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, rank, .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(rank, expr_region); + const remaining_cond_result = try self.unify(fresh_bool, remaining_cond_var, rank); 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, rank, .no_expectation) or does_fx; + try self.types.setVarContent(ModuleEnv.varFrom(remaining_branch.body), .err); } // Break to avoid cascading errors @@ -2135,9 +2848,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, rank, .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, rank); self.setDetailIfTypeMismatch(final_else_result, problem.TypeMismatchDetail{ .incompatible_if_branches = .{ .parent_if_expr = if_expr_idx, .last_if_branch = last_if_branch, @@ -2145,9 +2858,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.types.setVarRedirect(if_expr_var, branch_var); return does_fx; } @@ -2155,17 +2868,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, rank: Rank, 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, rank, .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); @@ -2178,23 +2890,19 @@ fn checkMatchExpr(self: *Self, expr_idx: CIR.Expr.Idx, match: CIR.Expr.Match) Al 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, rank, .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, rank); + 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; + does_fx = try self.checkExpr(first_branch.value, rank, .no_expectation) or does_fx; const branch_var = ModuleEnv.varFrom(first_branch.value); // Then iterate over the rest of the branches @@ -2206,11 +2914,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, rank, .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, rank); self.setDetailIfTypeMismatch(ptrn_result, problem.TypeMismatchDetail{ .incompatible_match_patterns = .{ .match_expr = expr_idx, .num_branches = @intCast(match.branches.span.len), @@ -2221,8 +2929,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, rank, .no_expectation) or does_fx; + const branch_result = try self.unify(branch_var, ModuleEnv.varFrom(branch.value), rank); self.setDetailIfTypeMismatch(branch_result, problem.TypeMismatchDetail{ .incompatible_match_branches = .{ .match_expr = expr_idx, .num_branches = @intCast(match.branches.span.len), @@ -2240,11 +2948,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, rank, .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, rank); self.setDetailIfTypeMismatch(ptrn_result, problem.TypeMismatchDetail{ .incompatible_match_patterns = .{ .match_expr = expr_idx, .num_branches = @intCast(match.branches.span.len), @@ -2255,7 +2963,7 @@ 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; + does_fx = try self.checkExpr(other_branch.value, rank, .no_expectation) or does_fx; try self.types.setVarContent(ModuleEnv.varFrom(other_branch.value), .err); } @@ -2267,6 +2975,196 @@ fn checkMatchExpr(self: *Self, expr_idx: CIR.Expr.Idx, match: CIR.Expr.Match) Al return does_fx; } +// unary minus // + +fn checkUnaryMinusExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_region: Region, rank: Rank, 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, rank, .no_expectation); + + // For unary minus, we constrain the operand and result to be numbers + const operand_var = @as(Var, ModuleEnv.varFrom(unary.expr)); + const result_var = @as(Var, ModuleEnv.varFrom(expr_idx)); + + // Create a fresh number variable for the operation + const num_content = Content{ .structure = .{ .num = .{ + .num_unbound = .{ + .int_requirements = Num.IntRequirements.init(), + .frac_requirements = Num.FracRequirements.init(), + }, + } } }; + const num_var = try self.freshFromContent(num_content, rank, expr_region); + + // Unify operand and result with the number type + _ = try self.unify(num_var, operand_var, rank); + _ = try self.unify(num_var, result_var, rank); + + return does_fx; +} + +// unary not // + +fn checkUnaryNotExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_region: Region, rank: Rank, unary: CIR.Expr.UnaryNot) Allocator.Error!bool { + const trace = tracy.trace(@src()); + defer trace.end(); + + // Check the operand expression + const does_fx = try self.checkExpr(unary.expr, rank, .no_expectation); + + // For unary not, we constrain the operand and result to be booleans + const operand_var = @as(Var, ModuleEnv.varFrom(unary.expr)); + const result_var = @as(Var, ModuleEnv.varFrom(expr_idx)); + + // Create a fresh boolean variable for the operation + const bool_var = try self.freshBool(rank, expr_region); + + // Unify operand and result with the boolean type + _ = try self.unify(bool_var, operand_var, rank); + _ = try self.unify(bool_var, result_var, rank); + + return does_fx; +} + +// binop // + +/// Check the types for a binary operation expression +fn checkBinopExpr( + self: *Self, + expr_idx: CIR.Expr.Idx, + expr_region: Region, + rank: Rank, + 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, rank, .no_expectation) or does_fx; + does_fx = try self.checkExpr(binop.rhs, rank, .no_expectation) or does_fx; + + switch (binop.op) { + .add, .sub, .mul, .div, .rem, .pow, .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_, rank, .{ .explicit = expr_region }); + const rhs_instantiated = try self.instantiateVar(expectation.var_, rank, .{ .explicit = expr_region }); + + if (expectation.from_annotation) { + _ = try self.unifyFromAnno(lhs_instantiated, lhs_var, rank); + _ = try self.unifyFromAnno(rhs_instantiated, rhs_var, rank); + } else { + _ = try self.unify(lhs_instantiated, lhs_var, rank); + _ = try self.unify(rhs_instantiated, rhs_var, rank); + } + }, + .no_expectation => { + // Start with empty requirements that can be constrained by operands + const num_content = Content{ .structure = .{ .num = .{ + .num_unbound = .{ + .int_requirements = Num.IntRequirements.init(), + .frac_requirements = Num.FracRequirements.init(), + }, + } } }; + const lhs_num_var = try self.freshFromContent(num_content, rank, expr_region); + const rhs_num_var = try self.freshFromContent(num_content, rank, expr_region); + + // Unify left and right operands with num + _ = try self.unify(lhs_num_var, lhs_var, rank); + _ = try self.unify(rhs_num_var, rhs_var, rank); + }, + } + + // Unify left and right together + _ = try self.unify(lhs_var, rhs_var, rank); + + // Set root expr. If unifications succeeded this will the the + // num, otherwise the propgate error + try self.types.setVarRedirect(expr_var, lhs_var); + }, + .lt, .gt, .le, .ge, .eq, .ne => { + // Ensure the operands are the same type + const result = try self.unify(lhs_var, rhs_var, rank); + + if (result.isOk()) { + const fresh_bool = try self.freshBool(rank, expr_region); + try self.types.setVarRedirect(expr_var, fresh_bool); + } else { + try self.updateVar(expr_var, .err, rank); + } + }, + .@"and" => { + const lhs_fresh_bool = try self.freshBool(rank, expr_region); + const lhs_result = try self.unify(lhs_fresh_bool, lhs_var, rank); + self.setDetailIfTypeMismatch(lhs_result, .{ .invalid_bool_binop = .{ + .binop_expr = expr_idx, + .problem_side = .lhs, + .binop = .@"and", + } }); + + const rhs_fresh_bool = try self.freshBool(rank, expr_region); + const rhs_result = try self.unify(rhs_fresh_bool, rhs_var, rank); + 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, rank); + + // Set root expr. If unifications succeeded this will the the + // num, otherwise the propgate error + try self.types.setVarRedirect(expr_var, lhs_var); + }, + .@"or" => { + const lhs_fresh_bool = try self.freshBool(rank, expr_region); + const lhs_result = try self.unify(lhs_fresh_bool, lhs_var, rank); + self.setDetailIfTypeMismatch(lhs_result, .{ .invalid_bool_binop = .{ + .binop_expr = expr_idx, + .problem_side = .lhs, + .binop = .@"and", + } }); + + const rhs_fresh_bool = try self.freshBool(rank, expr_region); + const rhs_result = try self.unify(rhs_fresh_bool, rhs_var, rank); + 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, rank); + + // Set root expr. If unifications succeeded this will the the + // num, otherwise the propagate error + try self.types.setVarRedirect(expr_var, lhs_var); + }, + .pipe_forward => { + // TODO + }, + .null_coalesce => { + // TODO + }, + } + + return does_fx; +} + // problems // /// If the provided result is a type mismatch problem, append the detail to the @@ -2830,3 +3728,162 @@ fn setProblemTypeMismatchDetail(self: *Self, problem_idx: problem.Problem.Idx, m // } // } // } + +// instantiate - OLD // + +// const RigidVarBehavior = union(enum) { +// use_cached_rigid_vars, +// rollback_rigid_vars, +// }; + +// /// Instantiate a variable +// fn instantiateVar( +// self: *Self, +// var_to_instantiate: Var, +// rigid_to_flex_subs: *Instantiate.RigidSubstitutions, +// region_behavior: InstantiateRegionBehavior, +// ) std.mem.Allocator.Error!Var { +// self.var_map.clearRetainingCapacity(); + +// var instantiate = Instantiate.init(self.types, self.cir.getIdentStore(), &self.var_map); +// var instantiate_ctx = Instantiate.Ctx{ +// .rigid_var_subs = rigid_to_flex_subs, +// }; +// const instantiated_var = try instantiate.instantiateVar(var_to_instantiate, &instantiate_ctx); + +// // 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(); +// while (iterator.next()) |x| { +// // Get the newly created var +// const fresh_var = x.value_ptr.*; +// try self.fillInRegionsThrough(fresh_var); + +// switch (region_behavior) { +// .explicit => |region| { +// self.setRegionAt(fresh_var, region); +// }, +// .use_root_instantiated => { +// self.setRegionAt(fresh_var, root_instantiated_region); +// }, +// .use_last_var => { +// const old_var = x.key_ptr.*; +// const old_region = self.regions.get(@enumFromInt(@intFromEnum(old_var))).*; +// self.setRegionAt(fresh_var, old_region); +// }, +// } +// } +// } + +// // Assert that we have regions for every type variable +// self.debugAssertArraysInSync(); + +// 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 // + +// 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, + 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; + + // 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 = 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; + }; + + 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, +) std.mem.Allocator.Error!Var { + // First, reset state + self.var_map.clearRetainingCapacity(); + + // Then, 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, + ); + + // 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; +} diff --git a/src/check/copy_import.zig b/src/check/copy_import.zig index 65682752e7..4dbca7cbf2 100644 --- a/src/check/copy_import.zig +++ b/src/check/copy_import.zig @@ -55,7 +55,11 @@ pub fn copyVar( 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.setVarDesc(placeholder_var, .{ + .content = dest_content, + .rank = types_mod.Rank.generalized, + .mark = types_mod.Mark.none, + }); return placeholder_var; } @@ -70,8 +74,8 @@ 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 = flex }, + .rigid => |rigid| Content{ .rigid = rigid }, .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) }, .err => Content.err, @@ -136,11 +140,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, }; @@ -179,9 +178,9 @@ fn copyNum( 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_poly => |poly_var| Num{ .num_poly = try copyVar(source_store, dest_store, poly_var, var_mapping, source_idents, dest_idents, allocator) }, + .int_poly => |poly_var| Num{ .int_poly = try copyVar(source_store, dest_store, poly_var, var_mapping, source_idents, dest_idents, allocator) }, + .frac_poly => |poly_var| Num{ .frac_poly = try copyVar(source_store, dest_store, poly_var, var_mapping, source_idents, dest_idents, allocator) }, .num_unbound => |unbound| Num{ .num_unbound = unbound }, .int_unbound => |unbound| Num{ .int_unbound = unbound }, .frac_unbound => |unbound| Num{ .frac_unbound = unbound }, diff --git a/src/check/mod.zig b/src/check/mod.zig index 6ed4d421b1..20e801eb5e 100644 --- a/src/check/mod.zig +++ b/src/check/mod.zig @@ -31,10 +31,14 @@ 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/compiled_builtins_test.zig")); + std.testing.refAllDecls(@import("test/unify_test.zig")); std.testing.refAllDecls(@import("test/cross_module_test.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/type_checking_integration.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/let_polymorphism_integration_test.zig")); + std.testing.refAllDecls(@import("test/num_type_inference_test.zig")); + std.testing.refAllDecls(@import("test/num_type_requirements_test.zig")); } diff --git a/src/check/occurs.zig b/src/check/occurs.zig index 6e7e4d0165..3cfa3e17cd 100644 --- a/src/check/occurs.zig +++ b/src/check/occurs.zig @@ -209,12 +209,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 +230,8 @@ const CheckOccurs = struct { const backing_var = self.types_store.getAliasBackingVar(alias); try self.occursSubVar(root, backing_var, ctx); }, - .flex_var => {}, - .rigid_var => {}, + .flex => {}, + .rigid => {}, .err => {}, } self.scratch.popSeen(); diff --git a/src/check/problem.zig b/src/check/problem.zig index a4e7f38505..e3e1fad730 100644 --- a/src/check/problem.zig +++ b/src/check/problem.zig @@ -96,6 +96,7 @@ 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, @@ -107,7 +108,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 }; @@ -144,6 +145,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, @@ -173,7 +179,7 @@ pub const InvalidBoolBinop = struct { /// 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, }; @@ -195,7 +201,8 @@ pub const ReportBuilder = struct { const Self = @This(); gpa: Allocator, - buf: std.array_list.Managed(u8), + snapshot_writer: snapshot.SnapshotWriter, + bytes_buf: std.array_list.Managed(u8), module_env: *ModuleEnv, can_ir: *const ModuleEnv, snapshots: *const snapshot.Store, @@ -215,7 +222,8 @@ pub const ReportBuilder = struct { ) Self { return .{ .gpa = gpa, - .buf = std.array_list.Managed(u8).init(gpa), + .snapshot_writer = snapshot.SnapshotWriter.init(gpa, snapshots, module_env.getIdentStore()), + .bytes_buf = std.array_list.Managed(u8).init(gpa), .module_env = module_env, .can_ir = can_ir, .snapshots = snapshots, @@ -228,7 +236,8 @@ pub const ReportBuilder = struct { /// Deinit report builder /// Only owned field is `buf` pub fn deinit(self: *Self) void { - self.buf.deinit(); + self.snapshot_writer.deinit(); + self.bytes_buf.deinit(); } /// Build a report for a problem @@ -239,69 +248,63 @@ 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); }, .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); } }, .type_apply_mismatch_arities => |data| { - return self.buildTypeApplyArityMismatchReport(&snapshot_writer, data); + return self.buildTypeApplyArityMismatchReport(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(), + .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"), + .bug => |_| return self.buildUnimplementedReport("bug"), } } @@ -310,19 +313,18 @@ 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.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.actual_snapshot); + const owned_actual = try report.addOwnedString(self.snapshot_writer.get()); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const owned_expected = try report.addOwnedString(self.buf.items[0..]); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.expected_snapshot); + const owned_expected = try report.addOwnedString(self.snapshot_writer.get()); // 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,8 +333,6 @@ 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))); @@ -349,7 +349,7 @@ pub const ReportBuilder = struct { // 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, .{ + return self.buildIncompatibleFnCallArg(types, .{ .fn_name = null, .arg_var = origin_var, .incompatible_arg_index = 0, // First argument @@ -374,18 +374,18 @@ pub const ReportBuilder = struct { ); try report.document.addLineBreak(); - if (types.from_annotation) { - try report.document.addText("The type annotation says it should have the type:"); - } else { - try report.document.addText("It has the type:"); - } + try report.document.addText("It has the type:"); 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:"); + if (types.from_annotation) { + try report.document.addText("But the type annotation says it should have the type:"); + } else { + try report.document.addText("But I expected it to be:"); + } try report.document.addLineBreak(); try report.document.addText(" "); try report.document.addAnnotated(owned_expected, .type_variable); @@ -419,7 +419,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 +426,21 @@ 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); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.expected_snapshot); + const expected_type = try report.addOwnedString(self.snapshot_writer.get()); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.actual_snapshot); - const actual_type = try report.addOwnedString(self.buf.items); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.actual_snapshot); + const actual_type = try report.addOwnedString(self.snapshot_writer.get()); - self.buf.clearRetainingCapacity(); - try appendOrdinal(&self.buf, data.incompatible_elem_index); - const expected_type_ordinal = 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 + 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 +460,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); @@ -555,16 +554,15 @@ 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); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.actual_snapshot); + const actual_type = try report.addOwnedString(self.snapshot_writer.get()); // Add description try report.document.addText("This "); @@ -633,7 +631,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 +645,17 @@ 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); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.actual_snapshot); + const actual_type = try report.addOwnedString(self.snapshot_writer.get()); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const expected_type = try report.addOwnedString(self.buf.items); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.expected_snapshot); + const expected_type = try report.addOwnedString(self.snapshot_writer.get()); - 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) { @@ -776,10 +773,103 @@ 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 + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.actual_snapshot); + const actual_type = try report.addOwnedString(self.snapshot_writer.get()); + + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.expected_snapshot); + const expected_type = try report.addOwnedString(self.snapshot_writer.get()); + + self.snapshot_writer.resetContext(); + 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 + self.snapshot_writer.resetContext(); + try report.document.addText("The first pattern 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.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.addText(" "); + try report.document.addAnnotated(expected_type, .type_variable); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + try report.document.addText("These two types can't never match!"); + try report.document.addLineBreak(); + try report.document.addLineBreak(); + + return report; + } + /// Build a report for incompatible match branches fn buildIncompatibleMatchPatterns( self: *Self, - snapshot_writer: *snapshot.SnapshotWriter, types: TypePair, data: IncompatibleMatchPatterns, ) !Report { @@ -787,25 +877,25 @@ 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); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.actual_snapshot); + const actual_type = try report.addOwnedString(self.snapshot_writer.get()); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const expected_type = try report.addOwnedString(self.buf.items); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.expected_snapshot); + const expected_type = try report.addOwnedString(self.snapshot_writer.get()); - 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); - 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(); + self.snapshot_writer.resetContext(); try report.document.addText("The pattern "); try report.document.addText(pattern_ord); try report.document.addText(" pattern in this "); @@ -814,7 +904,7 @@ pub const ReportBuilder = struct { try report.document.addText(" differs from previous ones:"); try report.document.addLineBreak(); } else { - self.buf.clearRetainingCapacity(); + self.snapshot_writer.resetContext(); try report.document.addText("The pattern in the "); try report.document.addText(branch_ord); try report.document.addText(" branch of this "); @@ -867,7 +957,7 @@ pub const ReportBuilder = struct { try report.document.addLineBreak(); // Show the type of the invalid branch - self.buf.clearRetainingCapacity(); + self.snapshot_writer.resetContext(); try report.document.addText("The "); try report.document.addText(branch_ord); try report.document.addText(" pattern has this type:"); @@ -902,7 +992,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 +1002,26 @@ 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); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.actual_snapshot); + const actual_type = try report.addOwnedString(self.snapshot_writer.get()); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const expected_type = try report.addOwnedString(self.buf.items); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.expected_snapshot); + const expected_type = try report.addOwnedString(self.snapshot_writer.get()); - 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(); + self.snapshot_writer.resetContext(); 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(); + self.snapshot_writer.resetContext(); try report.document.addText("The "); try report.document.addText(branch_ord); try report.document.addText(" branch's type in this "); @@ -1028,7 +1117,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 +1124,9 @@ 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); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.actual_snapshot); + const actual_type = try report.addOwnedString(self.snapshot_writer.get()); // Add description try report.document.addText("I'm having trouble with this bool operation:"); @@ -1120,7 +1208,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 +1219,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); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.writeTag(actual_tag, types.actual_snapshot); + const actual_tag_str = try report.addOwnedString(self.snapshot_writer.get()); // Create expected tag str const expected_content = self.snapshots.getContent(types.expected_snapshot); @@ -1168,20 +1255,20 @@ pub const ReportBuilder = struct { // 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); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.writeTag(expected_tag, types.expected_snapshot); + const expected_tag_str = try report.addOwnedString(self.snapshot_writer.get()); - 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); } else { - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.expected_snapshot); - const expected_type = try report.addOwnedString(self.buf.items); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.expected_snapshot); + const expected_type = try report.addOwnedString(self.snapshot_writer.get()); - 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); @@ -1195,11 +1282,9 @@ pub const ReportBuilder = struct { 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); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.writeTag(cur_expected_tag, types.expected_snapshot); + const cur_expected_tag_str = try report.addOwnedString(self.snapshot_writer.get()); try report.document.addLineBreak(); try report.document.addLineBreak(); @@ -1221,28 +1306,27 @@ pub const ReportBuilder = struct { /// 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.snapshot_writer.resetContext(); + try self.snapshot_writer.write(actual_arg_type); + const actual_type = try report.addOwnedString(self.snapshot_writer.get()); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(expected_arg_type); - const expected_type = try report.addOwnedString(self.buf.items); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(expected_arg_type); + const expected_type = try report.addOwnedString(self.snapshot_writer.get()); try report.document.addText("The "); try report.document.addText(arg_index); @@ -1269,7 +1353,7 @@ pub const ReportBuilder = struct { try report.document.addReflowingText("But "); if (data.fn_name) |fn_name_ident| { - self.buf.clearRetainingCapacity(); + self.snapshot_writer.resetContext(); const fn_name = try report.addOwnedString(self.can_ir.getIdent(fn_name_ident)); try report.document.addAnnotated(fn_name, .inline_code); } else { @@ -1287,30 +1371,29 @@ pub const ReportBuilder = struct { 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.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.expected_snapshot); + const first_type = try report.addOwnedString(self.snapshot_writer.get()); - self.buf.clearRetainingCapacity(); - try snapshot_writer.write(types.actual_snapshot); - const second_type = try report.addOwnedString(self.buf.items); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.actual_snapshot); + const second_type = try report.addOwnedString(self.snapshot_writer.get()); try report.document.addText("The "); try report.document.addText(first_arg_index); @@ -1397,7 +1480,7 @@ pub const ReportBuilder = struct { try report.document.addLineBreak(); if (data.fn_name) |fn_name_ident| { - self.buf.clearRetainingCapacity(); + self.snapshot_writer.resetContext(); const fn_name = try report.addOwnedString(self.can_ir.getIdent(fn_name_ident)); try report.document.addAnnotated(fn_name, .inline_code); } else { @@ -1413,7 +1496,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: { @@ -1430,18 +1512,16 @@ pub const ReportBuilder = struct { const type_name = try report.addOwnedString(self.can_ir.getIdent(data.type_name)); - 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.snapshot_writer.get()); - 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.snapshot_writer.get()); // 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); @@ -1469,15 +1549,14 @@ pub const ReportBuilder = struct { /// 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..]); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(data.expected_type); + const owned_expected = try report.addOwnedString(self.snapshot_writer.get()); const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.literal_var))); @@ -1510,15 +1589,14 @@ 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..]); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(data.expected_type); + const owned_expected = try report.addOwnedString(self.snapshot_writer.get()); const region = self.can_ir.store.regions.get(@enumFromInt(@intFromEnum(data.literal_var))); @@ -1557,7 +1635,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,9 +1694,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); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.expected_snapshot); + const expected_type = try report.addOwnedString(self.snapshot_writer.get()); try report.document.addText(" "); try report.document.addAnnotated(expected_type, .type_variable); try report.document.addLineBreak(); @@ -1635,9 +1712,9 @@ 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); + self.snapshot_writer.resetContext(); + try self.snapshot_writer.write(types.actual_snapshot); + const actual_type = try report.addOwnedString(self.snapshot_writer.get()); try report.document.addText(" "); try report.document.addAnnotated(actual_type, .type_variable); try report.document.addLineBreak(); @@ -1646,8 +1723,10 @@ pub const ReportBuilder = struct { } /// 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; } diff --git a/src/check/snapshot.zig b/src/check/snapshot.zig index dd17edf1fb..dee8aa2fe8 100644 --- a/src/check/snapshot.zig +++ b/src/check/snapshot.zig @@ -22,6 +22,8 @@ const MkSafeMultiList = collections.SafeMultiList; 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) /// @@ -39,6 +41,9 @@ 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, @@ -53,6 +58,7 @@ pub const Store = struct { 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), @@ -64,6 +70,7 @@ pub const Store = struct { pub fn deinit(self: *Self) void { self.contents.deinit(self.gpa); + self.seen_vars.deinit(self.gpa); self.content_indexes.deinit(self.gpa); self.record_fields.deinit(self.gpa); self.tags.deinit(self.gpa); @@ -76,13 +83,34 @@ pub const Store = struct { /// 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 { 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 + if (has_seen_var) { + return try self.contents.append(self.gpa, .recursive); + } + + // If not, add it to the seen list + try self.seen_vars.append(self.gpa, resolved.var_); + defer _ = self.seen_vars.pop(); + + return try self.deepCopyContent(store, resolved.var_, resolved.desc.content); } - fn deepCopyContent(self: *Self, store: *const TypesStore, content: Content) std.mem.Allocator.Error!SnapshotContentIdx { + fn deepCopyContent(self: *Self, store: *const TypesStore, 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 }, + .flex => |flex| SnapshotContent{ + .flex = .{ .ident = flex.name, .var_ = var_ }, + }, + .rigid => |rigid| SnapshotContent{ .rigid = rigid.name }, .alias => |alias| SnapshotContent{ .alias = try self.deepCopyAlias(store, alias) }, .structure => |flat_type| SnapshotContent{ .structure = try self.deepCopyFlatType(store, flat_type) }, .err => SnapshotContent.err, @@ -95,13 +123,11 @@ pub const Store = struct { 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); + const deep_content = try self.deepCopyVar(store, box_var); return SnapshotFlatType{ .box = deep_content }; }, .list => |list_var| { - const resolved = store.resolveVar(list_var); - const deep_content = try self.deepCopyContent(store, resolved.desc.content); + const deep_content = try self.deepCopyVar(store, list_var); return SnapshotFlatType{ .list = deep_content }; }, .list_unbound => { @@ -115,10 +141,6 @@ pub const Store = struct { .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), - } }, .empty_record => SnapshotFlatType.empty_record, .tag_union => |tag_union| SnapshotFlatType{ .tag_union = try self.deepCopyTagUnion(store, tag_union) }, .empty_tag_union => SnapshotFlatType.empty_tag_union, @@ -126,19 +148,16 @@ pub const Store = struct { } fn deepCopyAlias(self: *Self, store: *const TypesStore, alias: types.Alias) std.mem.Allocator.Error!SnapshotAlias { + const backing_var = store.getAliasBackingVar(alias); + const deep_backing = try self.deepCopyVar(store, 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); + const deep_arg = try self.deepCopyVar(store, arg_var); _ = try self.scratch_content.append(self.gpa, deep_arg); } @@ -148,7 +167,7 @@ pub const Store = struct { return SnapshotAlias{ .ident = alias.ident, - .backing = try self.deepCopyContent(store, backing_resolved.desc.content), + .backing = deep_backing, .vars = args_range, }; } @@ -161,8 +180,7 @@ 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); + const deep_elem = try self.deepCopyVar(store, elem_var); _ = try self.scratch_content.append(self.gpa, deep_elem); } @@ -177,19 +195,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); + .num_poly => |poly_var| { + const deep_poly = try self.deepCopyVar(store, poly_var); 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); + .int_poly => |poly_var| { + const deep_poly = try self.deepCopyVar(store, poly_var); return SnapshotNum{ .int_poly = deep_poly }; }, - .num_unbound => |requirements| { + .num_unbound => |unbound| { // For unbound types, we don't have a var to resolve, just return the requirements - return SnapshotNum{ .num_unbound = requirements }; + return SnapshotNum{ .num_unbound = .{ + .int_requirements = unbound.int_requirements, + .frac_requirements = unbound.frac_requirements, + } }; }, .int_unbound => |requirements| { // For unbound types, we don't have a var to resolve, just return the requirements @@ -199,9 +218,8 @@ pub const Store = struct { // 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); + .frac_poly => |poly_var| { + const deep_poly = try self.deepCopyVar(store, poly_var); return SnapshotNum{ .frac_poly = deep_poly }; }, .int_precision => |prec| { @@ -222,15 +240,13 @@ pub const Store = struct { // 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); + const deep_var = try self.deepCopyVar(store, backing_var); _ = try self.scratch_content.append(self.gpa, 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); + const deep_arg = try self.deepCopyVar(store, arg_var); _ = try self.scratch_content.append(self.gpa, deep_arg); } @@ -253,8 +269,7 @@ 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); + const deep_arg = try self.deepCopyVar(store, arg_var); _ = try self.scratch_content.append(self.gpa, deep_arg); } @@ -263,8 +278,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.deepCopyVar(store, func.ret); return SnapshotFunc{ .args = args_range, @@ -279,8 +293,7 @@ pub const Store = struct { 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.deepCopyVar(store, var_); const snapshot_field = SnapshotRecordField{ .name = name, @@ -306,8 +319,7 @@ 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.deepCopyVar(store, field.var_); const snapshot_field = SnapshotRecordField{ .name = field.name, @@ -322,8 +334,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.deepCopyVar(store, record.ext); return SnapshotRecord{ .fields = fields_range, @@ -347,8 +358,7 @@ 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); + const deep_tag_arg = try self.deepCopyVar(store, tag_arg_var); _ = try self.scratch_content.append(self.gpa, deep_tag_arg); } @@ -370,8 +380,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.deepCopyVar(store, tag_union.ext); return SnapshotTagUnion{ .tags = tags_range, @@ -395,10 +404,11 @@ pub const Store = struct { /// Snapshot types (no Var references!) pub const SnapshotContent = union(enum) { - flex_var: ?Ident.Idx, - rigid_var: Ident.Idx, + flex: struct { ident: ?Ident.Idx, var_: Var }, + rigid: Ident.Idx, alias: SnapshotAlias, structure: SnapshotFlatType, + recursive, err, }; @@ -423,7 +433,6 @@ pub const SnapshotFlatType = union(enum) { fn_unbound: SnapshotFunc, record: SnapshotRecord, record_unbound: SnapshotRecordFieldSafeList.Range, - record_poly: struct { record: SnapshotRecord, var_: SnapshotContentIdx }, empty_record, tag_union: SnapshotTagUnion, empty_tag_union, @@ -439,7 +448,7 @@ pub const SnapshotNum = union(enum) { num_poly: SnapshotContentIdx, int_poly: SnapshotContentIdx, frac_poly: SnapshotContentIdx, - num_unbound: types.Num.IntRequirements, + num_unbound: struct { int_requirements: types.Num.IntRequirements, frac_requirements: types.Num.FracRequirements }, int_unbound: types.Num.IntRequirements, frac_unbound: types.Num.FracRequirements, int_precision: types.Num.Int.Precision, @@ -502,7 +511,7 @@ const TypeContext = enum { pub const SnapshotWriter = struct { const Self = @This(); - writer: std.array_list.Managed(u8).Writer, + buf: std.array_list.Managed(u8), snapshots: *const Store, idents: *const Ident.Store, current_module_name: ?[]const u8, @@ -510,10 +519,14 @@ pub const SnapshotWriter = struct { other_modules: ?[]const *const ModuleEnv, next_name_index: u32, name_counters: std.EnumMap(TypeContext, u32), + flex_var_names_map: std.AutoHashMap(Var, FlexVarNameRange), + flex_var_names: std.array_list.Managed(u8), - pub fn init(writer: std.array_list.Managed(u8).Writer, snapshots: *const Store, idents: *const Ident.Store) Self { + const FlexVarNameRange = struct { start: usize, end: usize }; + + pub fn init(gpa: std.mem.Allocator, snapshots: *const Store, idents: *const Ident.Store) Self { return .{ - .writer = writer, + .buf = std.array_list.Managed(u8).init(gpa), .snapshots = snapshots, .idents = idents, .current_module_name = null, @@ -521,11 +534,13 @@ pub const SnapshotWriter = struct { .other_modules = null, .next_name_index = 0, .name_counters = std.EnumMap(TypeContext, u32).init(.{}), + .flex_var_names_map = std.AutoHashMap(Var, FlexVarNameRange).init(gpa), + .flex_var_names = std.array_list.Managed(u8).init(gpa), }; } pub fn initWithContext( - writer: std.array_list.Managed(u8).Writer, + gpa: std.mem.Allocator, snapshots: *const Store, idents: *const Ident.Store, current_module_name: []const u8, @@ -533,7 +548,7 @@ pub const SnapshotWriter = struct { other_modules: []const *const ModuleEnv, ) Self { return .{ - .writer = writer, + .buf = std.array_list.Managed(u8).init(gpa), .snapshots = snapshots, .idents = idents, .current_module_name = current_module_name, @@ -541,14 +556,34 @@ pub const SnapshotWriter = struct { .other_modules = other_modules, .next_name_index = 0, .name_counters = std.EnumMap(TypeContext, u32).init(.{}), + .flex_var_names_map = std.AutoHashMap(Var, FlexVarNameRange).init(gpa), + .flex_var_names = std.array_list.Managed(u8).init(gpa), }; } + pub fn deinit(self: *Self) void { + self.buf.deinit(); + self.flex_var_names_map.deinit(); + self.flex_var_names.deinit(); + } + pub fn resetContext(self: *Self) void { self.next_name_index = 0; self.name_counters = std.EnumMap(TypeContext, u32).init(.{}); + self.buf.clearRetainingCapacity(); + self.flex_var_names_map.clearRetainingCapacity(); + self.flex_var_names.clearRetainingCapacity(); } + pub fn get(self: *const Self) []const u8 { + return self.buf.items; + } + + const GenerateNameMode = union(enum) { + print, + persist_flex_var: Var, + }; + fn generateNextName(self: *Self) !void { // Generate name: a, b, ..., z, aa, ab, ..., az, ba, ... // Skip any names that already exist in the identifier store @@ -581,9 +616,8 @@ pub const SnapshotWriter = struct { 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); + try self.buf.writer().writeByte(c); } break; } @@ -592,8 +626,8 @@ pub const SnapshotWriter = struct { // 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}); + _ = try self.buf.writer().write("var"); + try self.buf.writer().print("{}", .{self.next_name_index}); } } @@ -640,7 +674,7 @@ pub const SnapshotWriter = struct { if (!exists) { // This name is available, write it to the writer for (candidate_name) |c| { - try self.writer.writeByte(c); + try self.buf.writer().writeByte(c); } found = true; } else { @@ -659,89 +693,6 @@ pub const SnapshotWriter = struct { } /// 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); @@ -749,22 +700,17 @@ pub const SnapshotWriter = struct { } /// 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 { + pub fn writeContent(self: *Self, content: SnapshotContent, context: TypeContext, content_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)); + .flex => |flex| { + if (flex.ident) |ident_idx| { + _ = try self.buf.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); + _ = try self.writeFlexVarName(flex.var_, content_idx, context, root_idx); } }, - .rigid_var => |ident_idx| { - _ = try self.writer.write(self.idents.getText(ident_idx)); + .rigid => |rigid_name| { + _ = try self.buf.writer().write(self.idents.getText(rigid_name)); }, .alias => |alias| { try self.writeAlias(alias, root_idx); @@ -772,28 +718,27 @@ pub const SnapshotWriter = struct { .structure => |flat_type| { try self.writeFlatType(flat_type, root_idx); }, + .recursive => { + _ = try self.buf.writer().write("RecursiveType"); + }, .err => { - _ = try self.writer.write("Error"); + _ = try self.buf.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..]; + _ = try self.buf.writer().write(self.idents.getText(alias.ident.ident_idx)); + const vars = self.snapshots.sliceVars(alias.vars); if (vars.len > 0) { - _ = try self.writer.write("("); + _ = try self.buf.writer().write("("); for (vars, 0..) |arg, i| { - if (i > 0) _ = try self.writer.write(", "); + if (i > 0) _ = try self.buf.writer().write(", "); try self.writeWithContext(arg, .General, root_idx); } - _ = try self.writer.write(")"); + _ = try self.buf.writer().write(")"); } } @@ -801,22 +746,22 @@ pub const SnapshotWriter = struct { pub fn writeFlatType(self: *Self, flat_type: SnapshotFlatType, root_idx: SnapshotContentIdx) Allocator.Error!void { switch (flat_type) { .str => { - _ = try self.writer.write("Str"); + _ = try self.buf.writer().write("Str"); }, .box => |sub_var| { - _ = try self.writer.write("Box("); + _ = try self.buf.writer().write("Box("); try self.writeWithContext(sub_var, .General, root_idx); - _ = try self.writer.write(")"); + _ = try self.buf.writer().write(")"); }, .list => |sub_var| { - _ = try self.writer.write("List("); + _ = try self.buf.writer().write("List("); try self.writeWithContext(sub_var, .ListContent, root_idx); - _ = try self.writer.write(")"); + _ = try self.buf.writer().write(")"); }, .list_unbound => { - _ = try self.writer.write("List(_"); + _ = try self.buf.writer().write("List(_"); try self.generateContextualName(.ListContent); - _ = try self.writer.write(")"); + _ = try self.buf.writer().write(")"); }, .tuple => |tuple| { try self.writeTuple(tuple, root_idx); @@ -842,18 +787,14 @@ pub const SnapshotWriter = struct { .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("{}"); + _ = try self.buf.writer().write("{}"); }, .tag_union => |tag_union| { try self.writeTagUnion(tag_union, root_idx); }, .empty_tag_union => { - _ = try self.writer.write("[]"); + _ = try self.buf.writer().write("[]"); }, } } @@ -861,17 +802,17 @@ pub const SnapshotWriter = struct { /// 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("("); + _ = try self.buf.writer().write("("); for (elems, 0..) |elem, i| { - if (i > 0) _ = try self.writer.write(", "); + if (i > 0) _ = try self.buf.writer().write(", "); try self.writeWithContext(elem, .TupleFieldContent, root_idx); } - _ = try self.writer.write(")"); + _ = try self.buf.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)); + _ = try self.buf.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); @@ -879,12 +820,12 @@ pub const SnapshotWriter = struct { vars = vars[1..]; if (vars.len > 0) { - _ = try self.writer.write("("); + _ = try self.buf.writer().write("("); for (vars, 0..) |arg, i| { - if (i > 0) _ = try self.writer.write(", "); + if (i > 0) _ = try self.buf.writer().write(", "); try self.writeWithContext(arg, .General, root_idx); } - _ = try self.writer.write(")"); + _ = try self.buf.writer().write(")"); } // Add origin information if it's from a different module @@ -893,9 +834,9 @@ pub const SnapshotWriter = struct { // 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(")"); + _ = try self.buf.writer().write(" (from "); + _ = try self.buf.writer().write(origin_module_name); + _ = try self.buf.writer().write(")"); } } } @@ -911,38 +852,38 @@ pub const SnapshotWriter = struct { // Write arguments if (args.len == 0) { - _ = try self.writer.write("({})"); + _ = try self.buf.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(", "); + if (i > 0) _ = try self.buf.writer().write(", "); try self.writeWithContext(arg, .FunctionArgument, root_idx); } } - _ = try self.writer.write(arrow); + _ = try self.buf.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("{ "); + _ = try self.buf.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.buf.writer().write(self.idents.getText(fields_slice.items(.name)[0])); + _ = try self.buf.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.buf.writer().write(", "); + _ = try self.buf.writer().write(self.idents.getText(name)); + _ = try self.buf.writer().write(": "); try self.writeWithContext(content, .RecordFieldContent, root_idx); } } @@ -952,73 +893,68 @@ pub const SnapshotWriter = struct { .structure => |flat_type| switch (flat_type) { .empty_record => {}, // Don't show empty extension else => { - if (fields_slice.len > 0) _ = try self.writer.write(", "); + if (fields_slice.len > 0) _ = try self.buf.writer().write(", "); try self.writeWithContext(record.ext, .RecordExtension, root_idx); }, }, else => { - if (fields_slice.len > 0) _ = try self.writer.write(", "); + if (fields_slice.len > 0) _ = try self.buf.writer().write(", "); try self.writeWithContext(record.ext, .RecordExtension, root_idx); }, } - _ = try self.writer.write(" }"); + _ = try self.buf.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("{}"); + _ = try self.buf.writer().write("{}"); return; } const fields_slice = self.snapshots.record_fields.sliceRange(fields); - _ = try self.writer.write("{ "); + _ = try self.buf.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.buf.writer().write(self.idents.getText(fields_slice.items(.name)[0])); + _ = try self.buf.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.buf.writer().write(", "); + _ = try self.buf.writer().write(self.idents.getText(name)); + _ = try self.buf.writer().write(": "); try self.writeWithContext(content, .RecordFieldContent, root_idx); } - _ = try self.writer.write(" }"); + _ = try self.buf.writer().write(" }"); } /// Write a tag union pub fn writeTagUnion(self: *Self, tag_union: SnapshotTagUnion, root_idx: SnapshotContentIdx) Allocator.Error!void { - _ = try self.writer.write("["); + _ = try self.buf.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(", "); + _ = try self.buf.writer().write(", "); } const tag = self.snapshots.tags.get(tag_idx); try self.writeTag(tag, root_idx); } - _ = try self.writer.write("]"); + _ = try self.buf.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)); + .flex => |flex| { + if (flex.ident) |ident_idx| { + _ = try self.buf.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); + _ = try self.writeFlexVarName(flex.var_, tag_union.ext, .TagUnionExtension, root_idx); } }, .structure => |flat_type| switch (flat_type) { @@ -1027,8 +963,8 @@ pub const SnapshotWriter = struct { try self.writeWithContext(tag_union.ext, .TagUnionExtension, root_idx); }, }, - .rigid_var => |ident_idx| { - _ = try self.writer.write(self.idents.getText(ident_idx)); + .rigid => |rigid_name| { + _ = try self.buf.writer().write(self.idents.getText(rigid_name)); }, else => { try self.writeWithContext(tag_union.ext, .TagUnionExtension, root_idx); @@ -1038,15 +974,15 @@ pub const SnapshotWriter = struct { /// 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)); + _ = try self.buf.writer().write(self.idents.getText(tag.name)); const args = self.snapshots.sliceVars(tag.args); if (args.len > 0) { - _ = try self.writer.write("("); + _ = try self.buf.writer().write("("); for (args, 0..) |arg, i| { - if (i > 0) _ = try self.writer.write(", "); + if (i > 0) _ = try self.buf.writer().write(", "); try self.writeWithContext(arg, .General, root_idx); } - _ = try self.writer.write(")"); + _ = try self.buf.writer().write(")"); } } @@ -1054,74 +990,222 @@ pub const SnapshotWriter = struct { 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.buf.writer().write("Num("); try self.writeWithContext(sub_var, .NumContent, root_idx); - _ = try self.writer.write(")"); + _ = try self.buf.writer().write(")"); }, .int_poly => |sub_var| { - _ = try self.writer.write("Int("); + _ = try self.buf.writer().write("Int("); try self.writeWithContext(sub_var, .NumContent, root_idx); - _ = try self.writer.write(")"); + _ = try self.buf.writer().write(")"); }, .frac_poly => |sub_var| { - _ = try self.writer.write("Frac("); + _ = try self.buf.writer().write("Frac("); try self.writeWithContext(sub_var, .NumContent, root_idx); - _ = try self.writer.write(")"); + _ = try self.buf.writer().write(")"); }, .num_unbound => |_| { - _ = try self.writer.write("Num(_"); + _ = try self.buf.writer().write("Num(_"); try self.generateContextualName(.NumContent); - _ = try self.writer.write(")"); + _ = try self.buf.writer().write(")"); }, .int_unbound => |_| { - _ = try self.writer.write("Int(_"); + _ = try self.buf.writer().write("Int(_"); try self.generateContextualName(.NumContent); - _ = try self.writer.write(")"); + _ = try self.buf.writer().write(")"); }, .frac_unbound => |_| { - _ = try self.writer.write("Frac(_"); + _ = try self.buf.writer().write("Frac(_"); try self.generateContextualName(.NumContent); - _ = try self.writer.write(")"); + _ = try self.buf.writer().write(")"); }, .int_precision => |prec| { - try self.writeIntType(prec); + try self.writeIntType(prec, .precision); }, .frac_precision => |prec| { - try self.writeFracType(prec); + try self.writeFracType(prec, .precision); }, .num_compact => |compact| { switch (compact) { .int => |prec| { - try self.writeIntType(prec); + try self.writeIntType(prec, .compacted); }, .frac => |prec| { - try self.writeFracType(prec); + try self.writeFracType(prec, .compacted); }, } }, } } - 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"), - }; + const NumPrecType = enum { precision, compacted }; + + fn writeIntType(self: *Self, prec: types.Num.Int.Precision, num_type: NumPrecType) std.mem.Allocator.Error!void { + switch (num_type) { + .compacted => { + _ = switch (prec) { + .u8 => try self.buf.writer().write("Num(Int(Unsigned8))"), + .i8 => try self.buf.writer().write("Num(Int(Signed8))"), + .u16 => try self.buf.writer().write("Num(Int(Unsigned16))"), + .i16 => try self.buf.writer().write("Num(Int(Signed16))"), + .u32 => try self.buf.writer().write("Num(Int(Unsigned32))"), + .i32 => try self.buf.writer().write("Num(Int(Signed32))"), + .u64 => try self.buf.writer().write("Num(Int(Unsigned64))"), + .i64 => try self.buf.writer().write("Num(Int(Signed64))"), + .u128 => try self.buf.writer().write("Num(Int(Unsigned128))"), + .i128 => try self.buf.writer().write("Num(Int(Signed128))"), + }; + }, + .precision => { + _ = switch (prec) { + .u8 => try self.buf.writer().write("Unsigned8"), + .i8 => try self.buf.writer().write("Signed8"), + .u16 => try self.buf.writer().write("Unsigned16"), + .i16 => try self.buf.writer().write("Signed16"), + .u32 => try self.buf.writer().write("Unsigned32"), + .i32 => try self.buf.writer().write("Signed32"), + .u64 => try self.buf.writer().write("Unsigned64"), + .i64 => try self.buf.writer().write("Signed64"), + .u128 => try self.buf.writer().write("Unsigned128"), + .i128 => try self.buf.writer().write("Signed128"), + }; + }, + } } - 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"), - }; + fn writeFracType(self: *Self, prec: types.Num.Frac.Precision, num_type: NumPrecType) std.mem.Allocator.Error!void { + switch (num_type) { + .compacted => { + _ = switch (prec) { + .f32 => try self.buf.writer().write("Num(Frac(Float32))"), + .f64 => try self.buf.writer().write("Num(Frac(Float64))"), + .dec => try self.buf.writer().write("Num(Frac(Decimal))"), + }; + }, + .precision => { + _ = switch (prec) { + .f32 => try self.buf.writer().write("Float32"), + .f64 => try self.buf.writer().write("Float64"), + .dec => try self.buf.writer().write("Decimal"), + }; + }, + } + } + + /// Generate a name for a flex var that may appear multiple times in the type + pub fn writeFlexVarName(self: *Self, flex_var: Var, _: SnapshotContentIdx, context: TypeContext, root_idx: SnapshotContentIdx) std.mem.Allocator.Error!void { + // Check if we've seen this flex var before. + if (self.flex_var_names_map.get(flex_var)) |range| { + // If so, then use that name + _ = try self.buf.writer().write( + self.flex_var_names.items[range.start..range.end], + ); + } else { + + // Check if this variable appears multiple times + const occurrences = self.countOccurrences(flex_var, root_idx); + + if (occurrences == 1) { + // If it appears once, then generate the contextual name + _ = try self.buf.writer().write("_"); + try self.generateContextualName(context); + } else { + // If it appears more than once, then we have to track the name we + // assign it so it appears consistently across the type str + + // Generate a new general var name. We do not use the context here + // because that may be the current context the var appears in, but + // the var may later appear in a different context + const buf_start = self.buf.items.len; + try self.generateContextualName(.General); + const buf_end = self.buf.items.len; + + // Then write down the name we generated for later + const flex_start = self.flex_var_names.items.len; + try self.flex_var_names.appendSlice(self.buf.items[buf_start..buf_end]); + const flex_end = self.flex_var_names.items.len; + try self.flex_var_names_map.put(flex_var, .{ .start = flex_start, .end = flex_end }); + } + } + } + + fn countOccurrences(self: *const Self, search_flex_var: Var, root_idx: SnapshotContentIdx) usize { + var count: usize = 0; + self.countContent(search_flex_var, root_idx, &count); + return count; + } + + fn countContent(self: *const Self, search_flex_var: Var, current_idx: SnapshotContentIdx, count: *usize) void { + const content = self.snapshots.contents.get(current_idx); + switch (content.*) { + .flex => |cur_flex| { + if (search_flex_var == cur_flex.var_) { + count.* += 1; + } + }, + .rigid, .recursive, .err => {}, + .alias => |alias| { + const args = self.snapshots.sliceVars(alias.vars); + for (args) |arg_idx| { + self.countContent(search_flex_var, arg_idx, count); + } + }, + .structure => |flat_type| { + self.countInFlatType(search_flex_var, flat_type, count); + }, + } + } + + fn countInFlatType(self: *const Self, search_flex_var: Var, flat_type: SnapshotFlatType, count: *usize) void { + switch (flat_type) { + .str, .empty_record, .empty_tag_union => {}, + .box => |sub_idx| self.countContent(search_flex_var, sub_idx, count), + .list => |sub_idx| self.countContent(search_flex_var, sub_idx, count), + .list_unbound, .num => {}, + .tuple => |tuple| { + const elems = self.snapshots.sliceVars(tuple.elems); + for (elems) |elem| { + self.countContent(search_flex_var, 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_flex_var, arg_idx, count); + } + }, + .fn_pure, .fn_effectful, .fn_unbound => |func| { + const args = self.snapshots.sliceVars(func.args); + for (args) |arg| { + self.countContent(search_flex_var, arg, count); + } + self.countContent(search_flex_var, func.ret, count); + }, + .record => |record| { + const fields = self.snapshots.record_fields.sliceRange(record.fields); + for (fields.items(.content)) |field_content| { + self.countContent(search_flex_var, field_content, count); + } + self.countContent(search_flex_var, 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_flex_var, field_content, 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_flex_var, arg_idx, count); + } + } + self.countContent(search_flex_var, tag_union.ext, count); + }, + } } }; diff --git a/src/check/test/TestEnv.zig b/src/check/test/TestEnv.zig new file mode 100644 index 0000000000..34abebabdc --- /dev/null +++ b/src/check/test/TestEnv.zig @@ -0,0 +1,334 @@ +//! 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 Check = @import("../Check.zig"); +const problem_mod = @import("../problem.zig"); + +const CommonEnv = base.CommonEnv; +const testing = std.testing; + +gpa: std.mem.Allocator, +module_env: *ModuleEnv, +parse_ast: *parse.AST, +can: *Can, +checker: Check, +type_writer: types.TypeWriter, + +module_envs: std.StringHashMap(*const ModuleEnv), +other_envs: std.array_list.Managed(*const ModuleEnv), + +/// 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 +pub fn initWithImport(source: []const u8, other_module_name: []const u8, other_module_env: *const ModuleEnv) !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.StringHashMap(*const ModuleEnv).init(gpa); + var other_envs = std.array_list.Managed(*const ModuleEnv).init(gpa); + + // Put the other module in the env map + try module_envs.put(other_module_name, other_module_env); + + const module_name = "Test"; + std.debug.assert(!std.mem.eql(u8, module_name, other_module_name)); + + // 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; + try module_env.common.calcLineStarts(gpa); + + const module_common_idents: Check.CommonIdents = .{ + .module_name = try module_env.insertIdent(base.Ident.for_text(module_name)), + .list = try module_env.insertIdent(base.Ident.for_text("List")), + .box = try module_env.insertIdent(base.Ident.for_text("Box")), + }; + + // 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(gpa, "test"); + can.* = try Can.init(module_env, parse_ast, &module_envs, .{}); + errdefer can.deinit(); + + try can.canonicalizeFile(); + try can.validateForChecking(); + + // Pull out the imported index + std.debug.assert(can.import_indices.size == 1); + const import_idx = can.import_indices.get(other_module_name).?; + std.debug.assert(@intFromEnum(import_idx) == 0); + try other_envs.append(other_module_env); + + // Type Check + var checker = try Check.init(gpa, &module_env.types, module_env, other_envs.items, &module_env.store.regions, module_common_idents); + 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, + .other_envs = other_envs, + }; +} + +/// Initialize where the provided source is an entire file +pub fn init(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); + + const module_envs = std.StringHashMap(*const ModuleEnv).init(gpa); + const other_envs = std.array_list.Managed(*const ModuleEnv).init(gpa); + + const module_name = "Test"; + + // 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; + try module_env.common.calcLineStarts(gpa); + + const module_common_idents: Check.CommonIdents = .{ + .module_name = try module_env.insertIdent(base.Ident.for_text(module_name)), + .list = try module_env.insertIdent(base.Ident.for_text("List")), + .box = try module_env.insertIdent(base.Ident.for_text("Box")), + }; + + // 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(gpa, "test"); + can.* = try Can.init(module_env, parse_ast, null, .{}); + errdefer can.deinit(); + + try can.canonicalizeFile(); + try can.validateForChecking(); + + // Type Check + var checker = try Check.init(gpa, &module_env.types, module_env, &.{}, &module_env.store.regions, module_common_idents); + 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, + .other_envs = other_envs, + }; +} + +/// Initialize where the provided source a single expression +pub fn initExpr(comptime source_expr: []const u8) !TestEnv { + const source_wrapper = + \\module [] + \\ + \\main = + ; + + var source: [source_wrapper.len + 1 + source_expr.len]u8 = undefined; + std.mem.copyForwards(u8, source[0..], source_wrapper); + std.mem.copyForwards(u8, source[source_wrapper.len..], " "); + std.mem.copyForwards(u8, source[source_wrapper.len + 1 ..], source_expr); + + return TestEnv.init(&source); +} + +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); + + self.module_envs.deinit(); + self.other_envs.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 assertLastDefType(self: *TestEnv, expected: []const u8) !void { + 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]; + + try testing.expectEqualStrings(expected, try self.type_writer.writeGet(ModuleEnv.varFrom(last_def_idx))); +} + +/// 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 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", + &.{}, + ); + defer report_builder.deinit(); + + var report = try report_builder.build(problem); + defer report.deinit(); + + try testing.expectEqualStrings(expected, report.title); +} + +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); + defer report.deinit(); + + report_buf.clearRetainingCapacity(); + try report.render(report_buf.writer(), .markdown); + + 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(); + + report_buf.clearRetainingCapacity(); + try report.render(report_buf.writer(), .markdown); + + 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(); + for (diagnostics) |d| { + var report = try self.module_env.diagnosticToReport(d, self.gpa, self.module_env.module_name); + defer report.deinit(); + + report_buf.clearRetainingCapacity(); + try report.render(report_buf.writer(), .markdown); + + 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", &.{}); + 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(); + + report_buf.clearRetainingCapacity(); + try report.render(report_buf.writer(), .markdown); + + 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/compiled_builtins_test.zig b/src/check/test/compiled_builtins_test.zig new file mode 100644 index 0000000000..03240b503e --- /dev/null +++ b/src/check/test/compiled_builtins_test.zig @@ -0,0 +1,209 @@ +//! Tests for compiled builtin modules (Set and Dict). +//! +//! These tests verify that the build-time compiled Set and Dict modules +//! can be loaded from their serialized .bin files and used in type checking. + +const std = @import("std"); +const base = @import("base"); +const types_mod = @import("types"); +const can = @import("can"); +const collections = @import("collections"); +const Check = @import("../Check.zig"); +const TestEnv = @import("./TestEnv.zig"); + +const ModuleEnv = can.ModuleEnv; +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) 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); + } +}; + +/// 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); + env.* = ModuleEnv{ + .gpa = gpa, + .common = serialized_ptr.common.deserialize(@as(i64, @intCast(base_ptr)), source).*, + .types = serialized_ptr.types.deserialize(@as(i64, @intCast(base_ptr)), gpa).*, + .module_kind = serialized_ptr.module_kind, + .all_defs = serialized_ptr.all_defs, + .all_statements = serialized_ptr.all_statements, + .exports = serialized_ptr.exports, + .builtin_statements = serialized_ptr.builtin_statements, + .external_decls = serialized_ptr.external_decls.deserialize(@as(i64, @intCast(base_ptr))).*, + .imports = serialized_ptr.imports.deserialize(@as(i64, @intCast(base_ptr)), gpa).*, + .module_name = module_name, + .diagnostics = serialized_ptr.diagnostics, + .store = serialized_ptr.store.deserialize(@as(i64, @intCast(base_ptr)), gpa).*, + }; + + return LoadedModule{ + .env = env, + .buffer = buffer, + .gpa = gpa, + }; +} + +test "compiled builtins - load Dict" { + const gpa = testing.allocator; + + const dict_source = "Dict := [EmptyDict].{}\n"; + var dict_loaded = try loadCompiledModule(gpa, compiled_builtins.dict_bin, "Dict", dict_source); + defer dict_loaded.deinit(); + + // Verify the module loaded + try testing.expectEqualStrings("Dict", dict_loaded.env.module_name); +} + +test "compiled builtins - load Set" { + const gpa = testing.allocator; + + const set_source = "import Dict\n\nSet := [EmptySet(Dict)].{}\n"; + var set_loaded = try loadCompiledModule(gpa, compiled_builtins.set_bin, "Set", set_source); + defer set_loaded.deinit(); + + // Verify the module loaded + try testing.expectEqualStrings("Set", set_loaded.env.module_name); +} + +test "compiled builtins - use Set and Dict together" { + const gpa = testing.allocator; + + // Load Dict first + const dict_source = "Dict := [EmptyDict].{}\n"; + var dict_loaded = try loadCompiledModule(gpa, compiled_builtins.dict_bin, "Dict", dict_source); + defer dict_loaded.deinit(); + + // Load Set (which imports Dict) + const set_source = "import Dict\n\nSet := [EmptySet(Dict)].{}\n"; + var set_loaded = try loadCompiledModule(gpa, compiled_builtins.set_bin, "Set", set_source); + defer set_loaded.deinit(); + + // Now create a test that uses both Set and Dict + const test_source = + \\module [] + \\ + \\import Set + \\import Dict + \\ + \\x : Set + \\x = Set.EmptySet(Dict.EmptyDict) + \\ + \\main = match x { + \\ Set.EmptySet(Dict.EmptyDict) => "empty" + \\} + ; + + // Create module environment with Set and Dict imported + var module_env: *ModuleEnv = try gpa.create(ModuleEnv); + errdefer gpa.destroy(module_env); + + module_env.* = try ModuleEnv.init(gpa, test_source); + errdefer module_env.deinit(); + + module_env.common.source = test_source; + module_env.module_name = "Test"; + try module_env.common.calcLineStarts(gpa); + + const module_common_idents: Check.CommonIdents = .{ + .module_name = try module_env.insertIdent(base.Ident.for_text("Test")), + .list = try module_env.insertIdent(base.Ident.for_text("List")), + .box = try module_env.insertIdent(base.Ident.for_text("Box")), + }; + + // Parse + const parse = @import("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(); + + if (parse_ast.hasErrors()) { + std.debug.print("Parse errors:\n", .{}); + for (parse_ast.tokenize_diagnostics.items) |diag| { + std.debug.print(" Tokenize: {any}\n", .{diag}); + } + for (parse_ast.parse_diagnostics.items) |diag| { + std.debug.print(" Parse: {any}\n", .{diag}); + } + return error.ParseError; + } + + // Set up module imports + var module_envs = std.StringHashMap(*const ModuleEnv).init(gpa); + defer module_envs.deinit(); + try module_envs.put("Set", set_loaded.env); + try module_envs.put("Dict", dict_loaded.env); + + // Canonicalize + try module_env.initCIRFields(gpa, "test"); + var can_result = try gpa.create(can.Can); + defer { + can_result.deinit(); + gpa.destroy(can_result); + } + + can_result.* = try can.Can.init(module_env, parse_ast, &module_envs, .{}); + try can_result.canonicalizeFile(); + try can_result.validateForChecking(); + + // Type check + var other_envs = std.array_list.Managed(*const ModuleEnv).init(gpa); + defer other_envs.deinit(); + try other_envs.append(set_loaded.env); + try other_envs.append(dict_loaded.env); + + var checker = try Check.init( + gpa, + &module_env.types, + module_env, + other_envs.items, + &module_env.store.regions, + module_common_idents, + ); + defer checker.deinit(); + + try checker.checkFile(); + + // Verify no type errors + try testing.expectEqual(0, checker.problems.problems.items.len); + + // Clean up module_env + module_env.deinit(); + gpa.destroy(module_env); +} diff --git a/src/check/test/cross_module_test.zig b/src/check/test/cross_module_test.zig index 40c5f5c609..97110a2f1a 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,103 @@ 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 type checking - monomorphic function passes" { + const source_a = + \\module [id_str] + \\ + \\id_str : Str -> Str + \\id_str = |s| s + ; + var test_env_a = try TestEnv.init(source_a); + defer test_env_a.deinit(); + 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.array_list.Managed(*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 = + \\module [] + \\ + \\import A + \\ + \\main : Str + \\main = A.id_str("hello") + ; + var test_env_b = try TestEnv.initWithImport(source_b, "A", test_env_a.module_env); + defer test_env_b.deinit(); + try test_env_b.assertLastDefType("Str"); } -test "cross-module type checking - polymorphic function" { - const allocator = testing.allocator; +test "cross-module type checking - monomorphic function fails" { + const source_a = + \\module [id_str] + \\ + \\id_str : Str -> Str + \\id_str = |s| s + ; + var test_env_a = try TestEnv.init(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.array_list.Managed(*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 = + \\module [] + \\ + \\import A + \\ + \\main : U8 + \\main = A.id_str(1) + ; + var test_env_b = try TestEnv.initWithImport(source_b, "A", test_env_a.module_env); + 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 type checking - polymorphic function passes" { + const source_a = + \\module [id] + \\ + \\id : a -> a + \\id = |s| s + ; + var test_env_a = try TestEnv.init(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.array_list.Managed(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.array_list.Managed(*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 = + \\module [] + \\ + \\import A + \\ + \\main : Str + \\main = A.id("hello") + ; + var test_env_b = try TestEnv.initWithImport(source_b, "A", test_env_a.module_env); + defer test_env_b.deinit(); + try test_env_b.assertLastDefType("Str"); } -test "cross-module type checking - type mismatch error" { - const allocator = testing.allocator; +test "cross-module type checking - polymorphic function with multiple uses passes" { + const source_a = + \\module [id] + \\ + \\id : a -> a + \\id = |s| s + ; + var test_env_a = try TestEnv.init(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.array_list.Managed(*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()); -} - -test "cross-module type checking - polymorphic instantiation" { - const allocator = testing.allocator; - - // 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.array_list.Managed(*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); -} - -test "cross-module type checking - preserves module A types" { - const allocator = testing.allocator; - - // 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.array_list.Managed(*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); -} - -test "cross-module type checking - three module chain monomorphic" { - const allocator = testing.allocator; - - // 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.array_list.Managed(*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); -} - -test "cross-module type checking - three module chain polymorphic" { - const allocator = testing.allocator; - - // 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.array_list.Managed(*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); -} - -test "cross-module type checking - partial polymorphic instantiation chain" { - const allocator = testing.allocator; - - // Module A exports: map : (a -> b), List a -> List b - 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: 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.array_list.Managed(*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.array_list.Managed(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.array_list.Managed(*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.array_list.Managed(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.array_list.Managed(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.array_list.Managed(*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.array_list.Managed(*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.array_list.Managed(*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); - } - } + const source_b = + \\module [] + \\ + \\import A + \\ + \\main : U64 + \\main = { + \\ a = A.id(10) + \\ b = A.id(15) + \\ _c = A.id("Hello") + \\ a + b + \\} + ; + var test_env_b = try TestEnv.initWithImport(source_b, "A", test_env_a.module_env); + defer test_env_b.deinit(); + try test_env_b.assertLastDefType("Num(Int(Unsigned64))"); } diff --git a/src/check/test/let_polymorphism_integration_test.zig b/src/check/test/let_polymorphism_integration_test.zig index 9b7d9a28ac..173a9a3942 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,7 @@ test "direct polymorphic identity usage" { \\ { a, b } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "{ a: Num(_size), b: Str }"); } test "higher-order function with polymorphic identity" { @@ -62,7 +36,7 @@ test "higher-order function with polymorphic identity" { \\ { a, b } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "{ a: Num(_size), b: Str }"); } test "let-polymorphism with function composition" { @@ -76,7 +50,7 @@ test "let-polymorphism with function composition" { \\ { result1 } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "Num(_size)"); } test "polymorphic empty list" { @@ -88,7 +62,7 @@ test "polymorphic empty list" { \\ { empty, nums, strs } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "{ empty: List(_elem), nums: List(Num(_size)), strs: List(Str) }"); } test "polymorphic cons function" { @@ -105,7 +79,7 @@ test "polymorphic cons function" { \\ { list1, list2 } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "TODO"); } test "polymorphic map function" { @@ -121,7 +95,7 @@ test "polymorphic map function" { const source = \\{ - \\ map = |f, xs| + \\ map = |f, xs| \\ if xs == [] then \\ [] \\ else @@ -131,7 +105,7 @@ test "polymorphic map function" { \\ { nums } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "TODO"); } test "polymorphic record constructor" { @@ -140,11 +114,11 @@ 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) + \\ pair3 = make_pair(True, False) \\ { pair1, pair2, pair3 } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "{ pair1: { first: Num(_size), second: Str }, pair2: { first: Str, second: Num(_size2) }, pair3: { first: Bool, second: Bool } }"); } test "polymorphic identity with various numeric types" { @@ -157,7 +131,7 @@ test "polymorphic identity with various numeric types" { \\ { int_val, float_val, bool_val } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "{ bool_val: Error, float_val: Num(Frac(_size)), int_val: Num(_size2) }"); } test "nested polymorphic data structures" { @@ -170,7 +144,7 @@ test "nested polymorphic data structures" { \\ { box1, box2, nested } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "{ box1: { value: Num(_size) }, box2: { value: Str }, nested: { value: { value: Num(_size2) } } }"); } test "polymorphic function in let binding" { @@ -185,31 +159,21 @@ test "polymorphic function in let binding" { \\ result \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "{ a: Num(_size), b: 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)); + try typeCheck(source, "{ swapped1: { first: Str, second: Num(_size) }, swapped2: { first: Num(_size2), second: Bool } }"); } test "polymorphic fold function" { @@ -236,7 +200,7 @@ test "polymorphic fold function" { \\ { sum, concat } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "TODO"); } test "polymorphic option type simulation" { @@ -250,7 +214,7 @@ test "polymorphic option type simulation" { \\ { opt1, opt2, opt3 } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "{ opt1: { tag: Str, value: Num(_size) }, opt2: { tag: Str, value: Str }, opt3: { tag: Str } }"); } test "polymorphic const function" { @@ -264,7 +228,7 @@ test "polymorphic const function" { \\ { num, str } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "{ num: Num(_size), str: Str }"); } test "shadowing of polymorphic values" { @@ -292,7 +256,7 @@ test "shadowing of polymorphic values" { \\ { a, inner, c } \\} ; - try testing.expect(try typeCheck(test_allocator, source)); + try typeCheck(source, "TODO"); } test "polymorphic pipe function" { @@ -300,11 +264,18 @@ 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: Num(_size), str_result: Num(_size2) }"); +} + +/// 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(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 index 0a108e471b..615a9149ff 100644 --- a/src/check/test/nominal_type_origin_test.zig +++ b/src/check/test/nominal_type_origin_test.zig @@ -3,11 +3,13 @@ const std = @import("std"); const base = @import("base"); const types_mod = @import("types"); +const can = @import("can"); const Check = @import("../Check.zig"); const snapshot = @import("../snapshot.zig"); const Ident = base.Ident; +const ModuleEnv = can.ModuleEnv; const testing = std.testing; const test_allocator = testing.allocator; @@ -38,19 +40,17 @@ test "nominal type origin - displays origin in snapshot writer" { // Test 1: Origin shown when type is from different module { - var buf = std.array_list.Managed(u8).init(test_allocator); - defer buf.deinit(); - var writer = snapshot.SnapshotWriter.init( - buf.writer(), + test_allocator, &snapshots, &idents, ); + defer writer.deinit(); writer.current_module_name = "CurrentModule"; try writer.writeNominalType(nominal_type, nominal_type_backing_idx); - const result = buf.items; + const result = writer.get(); // 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); @@ -58,9 +58,6 @@ test "nominal type origin - displays origin in snapshot writer" { // Test 2: Origin NOT shown when type is from same module { - var buf = std.array_list.Managed(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 }, @@ -69,15 +66,16 @@ test "nominal type origin - displays origin in snapshot writer" { }; var writer = snapshot.SnapshotWriter.init( - buf.writer(), + test_allocator, &snapshots, &idents, ); + defer writer.deinit(); writer.current_module_name = "CurrentModule"; try writer.writeNominalType(same_module_nominal, nominal_type_backing_idx); - const result = buf.items; + const result = writer.get(); // 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); @@ -101,15 +99,16 @@ test "nominal type origin - displays origin in snapshot writer" { }; var writer = snapshot.SnapshotWriter.init( - buf.writer(), + test_allocator, &snapshots, &idents, ); + defer writer.deinit(); writer.current_module_name = "CurrentModule"; try writer.writeNominalType(generic_nominal, nominal_type_backing_idx); - const result = buf.items; + const result = writer.get(); // 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); @@ -137,19 +136,17 @@ test "nominal type origin - works with no context" { .origin_module = module_ident, }; - var buf = std.array_list.Managed(u8).init(test_allocator); - defer buf.deinit(); - // Use the basic init without context var writer = snapshot.SnapshotWriter.init( - buf.writer(), + test_allocator, &snapshots, &idents, ); + defer writer.deinit(); try writer.writeNominalType(nominal_type, nominal_type_backing_idx); - const result = buf.items; + const result = writer.get(); // 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..c188f9b677 --- /dev/null +++ b/src/check/test/num_type_inference_test.zig @@ -0,0 +1,349 @@ +//! Tests for integer literal type inference +//! +//! This module contains unit tests that verify the correct type inferenece +//! of integer literals and integer expressions from CIR into the types store + +const std = @import("std"); +const testing = std.testing; +const base = @import("base"); +const types = @import("types"); +const parse = @import("parse"); +const builtins = @import("builtins"); +const Can = @import("can").Can; +const CIR = @import("can").CIR; +const ModuleEnv = @import("can").ModuleEnv; +const RocDec = builtins.dec.RocDec; +const TestEnv = @import("TestEnv.zig"); +const parseIntWithUnderscores = Can.parseIntWithUnderscores; +const Content = types.Content; + +test "infers type for small nums" { + const test_cases = [_]struct { source: []const u8, expected: []const u8 }{ + .{ .source = "1", .expected = "Num(_size)" }, + .{ .source = "-1", .expected = "Num(_size)" }, + .{ .source = "10", .expected = "Num(_size)" }, + .{ .source = "-10", .expected = "Num(_size)" }, + .{ .source = "255", .expected = "Num(_size)" }, + .{ .source = "-128", .expected = "Num(_size)" }, + .{ .source = "256", .expected = "Num(_size)" }, + .{ .source = "-129", .expected = "Num(_size)" }, + .{ .source = "32767", .expected = "Num(_size)" }, + .{ .source = "-32768", .expected = "Num(_size)" }, + .{ .source = "65535", .expected = "Num(_size)" }, + .{ .source = "-32769", .expected = "Num(_size)" }, + }; + + inline for (test_cases) |tc| { + var test_env = try TestEnv.initExpr(tc.source); + defer test_env.deinit(); + + try test_env.assertLastDefType(tc.expected); + } +} + +test "infers type for nums with specific requirements" { + const test_cases = [_]struct { + source: []const u8, + expected_sign_needed: bool, + expected_bits_needed: types.Num.Int.BitsNeeded, + }{ + .{ .source = "127", .expected_sign_needed = false, .expected_bits_needed = .@"7" }, + .{ .source = "128", .expected_sign_needed = false, .expected_bits_needed = .@"8" }, + .{ .source = "255", .expected_sign_needed = false, .expected_bits_needed = .@"8" }, + .{ .source = "256", .expected_sign_needed = false, .expected_bits_needed = .@"9_to_15" }, + .{ .source = "-128", .expected_sign_needed = true, .expected_bits_needed = .@"7" }, + .{ .source = "-129", .expected_sign_needed = true, .expected_bits_needed = .@"8" }, + .{ .source = "32767", .expected_sign_needed = false, .expected_bits_needed = .@"9_to_15" }, + .{ .source = "32768", .expected_sign_needed = false, .expected_bits_needed = .@"16" }, + .{ .source = "65535", .expected_sign_needed = false, .expected_bits_needed = .@"16" }, + .{ .source = "65536", .expected_sign_needed = false, .expected_bits_needed = .@"17_to_31" }, + }; + + inline for (test_cases) |tc| { + var test_env = try TestEnv.initExpr(tc.source); + defer test_env.deinit(); + + const typ = (try test_env.getLastExprType()).content; + + try testing.expect(typ == .structure); + try testing.expect(typ.structure == .num); + try testing.expect(typ.structure.num == .num_unbound); + + const reqs = typ.structure.num.num_unbound; + + try testing.expectEqual(tc.expected_sign_needed, reqs.int_requirements.sign_needed); + try testing.expectEqual( + tc.expected_bits_needed.toBits(), + reqs.int_requirements.bits_needed, + ); + + try testing.expectEqual(true, reqs.frac_requirements.fits_in_f32); + try testing.expectEqual(true, reqs.frac_requirements.fits_in_dec); + } +} + +test "infers num requirements correctly" { + const test_cases = [_]struct { + source: []const u8, + expected_sign_needed: bool, + expected_bits_needed: types.Num.Int.BitsNeeded, + }{ + // 255 needs 8 bits and no sign + .{ .source = "255", .expected_sign_needed = false, .expected_bits_needed = .@"8" }, + // 256 needs 9-15 bits and no sign + .{ .source = "256", .expected_sign_needed = false, .expected_bits_needed = .@"9_to_15" }, + // -1 needs sign and 7 bits + .{ .source = "-1", .expected_sign_needed = true, .expected_bits_needed = .@"7" }, + // 65535 needs 16 bits and no sign + .{ .source = "65535", .expected_sign_needed = false, .expected_bits_needed = .@"16" }, + // 65536 needs 17-31 bits and no sign + .{ .source = "65536", .expected_sign_needed = false, .expected_bits_needed = .@"17_to_31" }, + }; + + inline for (test_cases) |tc| { + var test_env = try TestEnv.initExpr(tc.source); + defer test_env.deinit(); + + const typ = (try test_env.getLastExprType()).content; + + try testing.expect(typ == .structure); + try testing.expect(typ.structure == .num); + try testing.expect(typ.structure.num == .num_unbound); + + const reqs = typ.structure.num.num_unbound; + + try testing.expectEqual(tc.expected_sign_needed, reqs.int_requirements.sign_needed); + try testing.expectEqual( + tc.expected_bits_needed.toBits(), + reqs.int_requirements.bits_needed, + ); + + try testing.expectEqual(true, reqs.frac_requirements.fits_in_f32); + try testing.expectEqual(true, reqs.frac_requirements.fits_in_dec); + } +} + +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(source); + defer test_env.deinit(); + + const typ = (try test_env.getLastExprType()).content; + try testing.expect(typ == .err); + } +} + +test "edge case: negative 0" { + const source = "-0"; + var test_env = try TestEnv.initExpr(source); + defer test_env.deinit(); + + const typ = (try test_env.getLastExprType()).content; + + try testing.expect(typ == .structure); + try testing.expect(typ.structure == .num); + try testing.expect(typ.structure.num == .num_unbound); + + const reqs = typ.structure.num.num_unbound; + + try testing.expectEqual(false, reqs.int_requirements.sign_needed); + try testing.expectEqual(7, reqs.int_requirements.bits_needed); + + try testing.expectEqual(true, reqs.frac_requirements.fits_in_f32); + try testing.expectEqual(true, reqs.frac_requirements.fits_in_dec); +} + +test "edge case: positive 0" { + const source = "0"; + var test_env = try TestEnv.initExpr(source); + defer test_env.deinit(); + + const typ = (try test_env.getLastExprType()).content; + + try testing.expect(typ == .structure); + try testing.expect(typ.structure == .num); + try testing.expect(typ.structure.num == .num_unbound); + + const reqs = typ.structure.num.num_unbound; + + try testing.expectEqual(false, reqs.int_requirements.sign_needed); + try testing.expectEqual(7, reqs.int_requirements.bits_needed); + + try testing.expectEqual(true, reqs.frac_requirements.fits_in_f32); + try testing.expectEqual(true, reqs.frac_requirements.fits_in_dec); +} + +test "infer hexadecimal literals as unbound integer" { + const test_cases = [_]struct { + source: []const u8, + expected_sign_needed: bool, + expected_bits_needed: types.Num.Int.BitsNeeded, + }{ + // Basic hex literals + .{ .source = "0x0", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "0x1", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "0xFF", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(1) }, + .{ .source = "0x100", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(2) }, + .{ .source = "0xFFFF", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(3) }, + .{ .source = "0x10000", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(4) }, + .{ .source = "0xFFFFFFFF", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(5) }, + .{ .source = "0x100000000", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(6) }, + .{ .source = "0xFFFFFFFFFFFFFFFF", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(7) }, + + // Hex with underscores + .{ .source = "0x1_000", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(2) }, + .{ .source = "0xFF_FF", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(3) }, + .{ .source = "0x1234_5678_9ABC_DEF0", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(6) }, + + // Negative hex literals + .{ .source = "-0x1", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "-0x80", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "-0x81", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(1) }, + .{ .source = "-0x8000", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(2) }, + .{ .source = "-0x8001", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(3) }, + .{ .source = "-0x80000000", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(4) }, + .{ .source = "-0x80000001", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(5) }, + .{ .source = "-0x8000000000000000", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(6) }, + .{ .source = "-0x8000000000000001", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(7) }, + }; + + inline for (test_cases) |tc| { + var test_env = try TestEnv.initExpr(tc.source); + defer test_env.deinit(); + + const typ = (try test_env.getLastExprType()).content; + + try testing.expect(typ == .structure); + try testing.expect(typ.structure == .num); + try testing.expect(typ.structure.num == .num_poly); + + const int_typ = test_env.module_env.types.resolveVar(typ.structure.num.num_poly).desc.content; + + try testing.expect(int_typ == .structure); + try testing.expect(int_typ.structure == .num); + try testing.expect(int_typ.structure.num == .int_unbound); + + const reqs = int_typ.structure.num.int_unbound; + + try testing.expectEqual(tc.expected_sign_needed, reqs.sign_needed); + try testing.expectEqual(tc.expected_bits_needed.toBits(), reqs.bits_needed); + } +} + +test "infer binary literals as unbound integer" { + const test_cases = [_]struct { + source: []const u8, + expected_sign_needed: bool, + expected_bits_needed: types.Num.Int.BitsNeeded, + }{ + // Basic binary literals + .{ .source = "0b0", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "0b1", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "0b10", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "0b11111111", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(1) }, + .{ .source = "0b100000000", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(2) }, + .{ .source = "0b1111111111111111", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(3) }, + .{ .source = "0b10000000000000000", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(4) }, + + // Binary with underscores + .{ .source = "0b11_11", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "0b1111_1111", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(1) }, + .{ .source = "0b1_0000_0000", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(2) }, + .{ .source = "0b1010_1010_1010_1010", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(3) }, + + // Negative binary + .{ .source = "-0b1", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "-0b10000000", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "-0b10000001", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(1) }, + .{ .source = "-0b1000000000000000", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(2) }, + .{ .source = "-0b1000000000000001", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(3) }, + }; + + inline for (test_cases) |tc| { + var test_env = try TestEnv.initExpr(tc.source); + defer test_env.deinit(); + + const typ = (try test_env.getLastExprType()).content; + + try testing.expect(typ == .structure); + try testing.expect(typ.structure == .num); + try testing.expect(typ.structure.num == .num_poly); + + const int_typ = test_env.module_env.types.resolveVar(typ.structure.num.num_poly).desc.content; + + try testing.expect(int_typ == .structure); + try testing.expect(int_typ.structure == .num); + try testing.expect(int_typ.structure.num == .int_unbound); + + const reqs = int_typ.structure.num.int_unbound; + + try testing.expectEqual(tc.expected_sign_needed, reqs.sign_needed); + try testing.expectEqual(tc.expected_bits_needed.toBits(), reqs.bits_needed); + } +} + +test "infer octal literals as unbound integer" { + const test_cases = [_]struct { + source: []const u8, + expected_sign_needed: bool, + expected_bits_needed: types.Num.Int.BitsNeeded, + }{ + // Basic octal literals + .{ .source = "0o0", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "0o1", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "0o7", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "0o10", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "0o377", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(1) }, + .{ .source = "0o400", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(2) }, + .{ .source = "0o177777", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(3) }, + .{ .source = "0o200000", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(4) }, + + // Octal with underscores + .{ .source = "0o377_377", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(4) }, + .{ .source = "0o1_234_567", .expected_sign_needed = false, .expected_bits_needed = @enumFromInt(4) }, + + // Negative octal literals + .{ .source = "-0o1", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "-0o100", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "-0o200", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(0) }, + .{ .source = "-0o201", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(1) }, + .{ .source = "-0o100000", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(2) }, + .{ .source = "-0o100001", .expected_sign_needed = true, .expected_bits_needed = @enumFromInt(3) }, + }; + + inline for (test_cases) |tc| { + var test_env = try TestEnv.initExpr(tc.source); + defer test_env.deinit(); + + const typ = (try test_env.getLastExprType()).content; + + try testing.expect(typ == .structure); + try testing.expect(typ.structure == .num); + try testing.expect(typ.structure.num == .num_poly); + + const int_typ = test_env.module_env.types.resolveVar(typ.structure.num.num_poly).desc.content; + + try testing.expect(int_typ == .structure); + try testing.expect(int_typ.structure == .num); + try testing.expect(int_typ.structure.num == .int_unbound); + + const reqs = int_typ.structure.num.int_unbound; + + try testing.expectEqual(tc.expected_sign_needed, reqs.sign_needed); + try testing.expectEqual(tc.expected_bits_needed.toBits(), reqs.bits_needed); + } +} 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..701751d25e --- /dev/null +++ b/src/check/test/num_type_requirements_test.zig @@ -0,0 +1,283 @@ +//! 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 TestEnv = @import("./TestEnv.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 "U8: 255 fits" { + const source = + \\{ + \\ x : U8 + \\ x = 50 + \\ + \\ x + 255 + \\} + ; + + var test_env = try TestEnv.initExpr(source); + defer test_env.deinit(); + try test_env.assertLastDefType("Num(Int(Unsigned8))"); +} + +test "U8: 256 does not fit" { + const source = + \\{ + \\ x : U8 + \\ x = 50 + \\ + \\ x + 256 + \\} + ; + + var test_env = try TestEnv.initExpr(source); + defer test_env.deinit(); + try test_env.assertOneTypeError("NUMBER DOES NOT FIT IN TYPE"); +} + +test "U8: negative does not fit" { + const source = + \\{ + \\ x : U8 + \\ x = 50 + \\ + \\ x + -1 + \\} + ; + + var test_env = try TestEnv.initExpr(source); + defer test_env.deinit(); + try test_env.assertOneTypeError("NEGATIVE UNSIGNED INTEGER"); +} + +test "I8: -128 fits" { + const source = + \\{ + \\ x : I8 + \\ x = 1 + \\ + \\ x + -128 + \\} + ; + + var test_env = try TestEnv.initExpr(source); + defer test_env.deinit(); + try test_env.assertLastDefType("Num(Int(Signed8))"); +} + +test "I8: -129 does not fit" { + const source = + \\{ + \\ x : I8 + \\ x = 1 + \\ + \\ x + -129 + \\} + ; + + var test_env = try TestEnv.initExpr(source); + defer test_env.deinit(); + try test_env.assertOneTypeError("NUMBER DOES NOT FIT IN TYPE"); +} + +test "F32: fits" { + const source = + \\{ + \\ x : F32 + \\ x = 1 + \\ + \\ x + 10.1 + \\} + ; + + var test_env = try TestEnv.initExpr(source); + defer test_env.deinit(); + try test_env.assertLastDefType("Num(Frac(Float32))"); +} + +// TODO: Move these to unify + +// 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/type_checking_integration.zig b/src/check/test/type_checking_integration.zig new file mode 100644 index 0000000000..bcaec62b9c --- /dev/null +++ b/src/check/test/type_checking_integration.zig @@ -0,0 +1,1117 @@ +//! 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 assertExprTypeCheckPass(source, "Num(_size)"); +} + +test "check type - num - int suffix 1" { + const source = + \\10u8 + ; + try assertExprTypeCheckPass(source, "Num(Int(Unsigned8))"); +} + +test "check type - num - int suffix 2" { + const source = + \\10i128 + ; + try assertExprTypeCheckPass(source, "Num(Int(Signed128))"); +} + +test "check type - num - int big" { + const source = + \\{ + \\ e : U128 + \\ e = 340282366920938463463374607431768211455 + \\ + \\ e + \\} + ; + try assertExprTypeCheckPass(source, "Num(Int(Unsigned128))"); +} + +test "check type - num - float" { + const source = + \\10.1 + ; + try assertExprTypeCheckPass(source, "Num(Frac(_size))"); +} + +test "check type - num - float suffix 1" { + const source = + \\10.1f32 + ; + try assertExprTypeCheckPass(source, "Num(Frac(Float32))"); +} + +test "check type - num - float suffix 2" { + const source = + \\10.1f64 + ; + try assertExprTypeCheckPass(source, "Num(Frac(Float64))"); +} + +test "check type - num - float suffix 3" { + const source = + \\10.1dec + ; + try assertExprTypeCheckPass(source, "Num(Frac(Decimal))"); +} + +// primitives - strs // + +test "check type - str" { + const source = + \\"hello" + ; + try assertExprTypeCheckPass(source, "Str"); +} + +// primitives - lists // + +test "check type - list empty" { + const source = + \\[] + ; + try assertExprTypeCheckPass(source, "List(_elem)"); +} + +test "check type - list - same elems 1" { + const source = + \\["hello", "world"] + ; + try assertExprTypeCheckPass(source, "List(Str)"); +} + +test "check type - list - same elems 2" { + const source = + \\[100, 200] + ; + try assertExprTypeCheckPass(source, "List(Num(_size))"); +} + +test "check type - list - 1st elem more specific coreces 2nd elem" { + const source = + \\[100u64, 200] + ; + try assertExprTypeCheckPass(source, "List(Num(Int(Unsigned64)))"); +} + +test "check type - list - 2nd elem more specific coreces 1st elem" { + const source = + \\[100, 200u32] + ; + try assertExprTypeCheckPass(source, "List(Num(Int(Unsigned32)))"); +} + +test "check type - list - diff elems 1" { + const source = + \\["hello", 10] + ; + try assertExprTypeCheckFail(source, "INCOMPATIBLE LIST ELEMENTS"); +} + +// number requirements // + +test "check type - num - cannot coerce 500 to u8" { + const source = + \\[500, 200u8] + ; + try assertExprTypeCheckFail(source, "NUMBER DOES NOT FIT IN TYPE"); +} + +// records // + +test "check type - record" { + const source = + \\{ + \\ hello: "Hello", + \\ world: 10, + \\} + ; + try assertExprTypeCheckPass(source, "{ hello: Str, world: Num(_size) }"); +} + +// tags // + +test "check type - tag" { + const source = + \\MyTag + ; + try assertExprTypeCheckPass(source, "[MyTag]_others"); +} + +test "check type - tag - args" { + const source = + \\MyTag("hello", 1) + ; + try assertExprTypeCheckPass(source, "[MyTag(Str, Num(_size))]_others"); +} + +// blocks // + +test "check type - block - return expr" { + const source = + \\{ + \\ "Hello" + \\} + ; + try assertExprTypeCheckPass(source, "Str"); +} + +test "check type - block - implicit empty record" { + const source = + \\{ + \\ _test = "hello" + \\} + ; + try assertExprTypeCheckPass(source, "{}"); +} + +test "check type - block - local value decl" { + const source = + \\{ + \\ test = "hello" + \\ + \\ test + \\} + ; + try assertExprTypeCheckPass(source, "Str"); +} + +// function // + +test "check type - def - value" { + const source = + \\module [] + \\ + \\pairU64 = "hello" + ; + try assertFileTypeCheckPass(source, "Str"); +} + +test "check type - def - func" { + const source = + \\module [] + \\ + \\id = |_| 20 + ; + try assertFileTypeCheckPass(source, "_arg -> Num(_size)"); +} + +test "check type - def - id without annotation" { + const source = + \\module [] + \\ + \\id = |x| x + ; + try assertFileTypeCheckPass(source, "a -> a"); +} + +test "check type - def - id with annotation" { + const source = + \\module [] + \\ + \\id : a -> a + \\id = |x| x + ; + try assertFileTypeCheckPass(source, "a -> a"); +} + +test "check type - def - func with annotation 1" { + const source = + \\module [] + \\ + \\id : x -> Str + \\id = |_| "test" + ; + try assertFileTypeCheckPass(source, "x -> Str"); +} + +test "check type - def - func with annotation 2" { + const source = + \\module [] + \\ + \\id : x -> Num(_size) + \\id = |_| 15 + ; + try assertFileTypeCheckPass(source, "x -> Num(_size)"); +} + +test "check type - def - nested lambda" { + const source = + \\module [] + \\ + \\id = (((|a| |b| |c| a + b + c)(100))(20))(3) + ; + try assertFileTypeCheckPass(source, "Num(_size)"); +} + +// calling functions + +test "check type - def - monomorphic id" { + const source = + \\module [] + \\ + \\idStr : Str -> Str + \\idStr = |x| x + \\ + \\test = idStr("hello") + ; + try assertFileTypeCheckPass(source, "Str"); +} + +test "check type - def - polymorphic id 1" { + const source = + \\module [] + \\ + \\id : x -> x + \\id = |x| x + \\ + \\test = id(5) + ; + try assertFileTypeCheckPass(source, "Num(_size)"); +} + +test "check type - def - polymorphic id 2" { + const source = + \\module [] + \\ + \\id : x -> x + \\id = |x| x + \\ + \\test = (id(5), id("hello")) + ; + try assertFileTypeCheckPass(source, "(Num(_size), Str)"); +} + +test "check type - def - polymorphic higher order 1" { + const source = + \\module [] + \\ + \\f = |g, v| g(v) + ; + try assertFileTypeCheckPass(source, "a -> b, a -> b"); +} + +test "check type - top level polymorphic function is generalized" { + const source = + \\module [] + \\ + \\id = |x| x + \\ + \\main = { + \\ a = id(42) + \\ _b = id("hello") + \\ a + \\} + ; + try assertFileTypeCheckPass(source, "Num(_size)"); +} + +test "check type - let-def polymorphic function is generalized" { + const source = + \\module [] + \\ + \\main = { + \\ id = |x| x + \\ a = id(42) + \\ b = id("hello") + \\ a + \\} + ; + try assertFileTypeCheckPass(source, "Num(_size)"); +} + +test "check type - polymorphic function function param should be constrained" { + const source = + \\module [] + \\ + \\id = |x| x + \\ + \\use_twice = |f| { + \\ a = f(42) + \\ b = f("hello") + \\ a + \\} + \\result = use_twice(id) + ; + try assertFileTypeCheckFail(source, "TYPE MISMATCH"); +} + +// type aliases // + +test "check type - basic alias" { + const source = + \\module [] + \\ + \\MyAlias : Str + \\ + \\x : MyAlias + \\x = "hello" + ; + try assertFileTypeCheckPass(source, "MyAlias"); +} + +test "check type - alias with arg" { + const source = + \\module [] + \\ + \\MyListAlias(a) : List(a) + \\ + \\x : MyListAlias(Num(size)) + \\x = [15] + ; + try assertFileTypeCheckPass(source, "MyListAlias(Num(size))"); +} + +test "check type - alias with mismatch arg" { + const source = + \\module [] + \\ + \\MyListAlias(a) : List(a) + \\ + \\x : MyListAlias(Str) + \\x = [15] + ; + try assertFileTypeCheckFail(source, "TYPE MISMATCH"); +} + +// nominal types // + +test "check type - basic nominal" { + const source = + \\module [] + \\ + \\MyNominal := [MyNominal] + \\ + \\x : MyNominal + \\x = MyNominal.MyNominal + ; + try assertFileTypeCheckPass(source, "MyNominal"); +} + +test "check type - nominal with tag arg" { + const source = + \\module [] + \\ + \\MyNominal := [MyNominal(Str)] + \\ + \\x : MyNominal + \\x = MyNominal.MyNominal("hello") + ; + try assertFileTypeCheckPass(source, "MyNominal"); +} + +test "check type - nominal with type and tag arg" { + const source = + \\module [] + \\ + \\MyNominal(a) := [MyNominal(a)] + \\ + \\x : MyNominal(U8) + \\x = MyNominal.MyNominal(10) + ; + try assertFileTypeCheckPass(source, "MyNominal(Num(Int(Unsigned8)))"); +} + +test "check type - nominal with with rigid vars" { + const source = + \\module [] + \\ + \\Pair(a) := [Pair(a, a)] + \\ + \\pairU64 : Pair(U64) + \\pairU64 = Pair.Pair(1, 2) + ; + try assertFileTypeCheckPass(source, "Pair(Num(Int(Unsigned64)))"); +} + +test "check type - nominal with with rigid vars mismatch" { + const source = + \\module [] + \\ + \\Pair(a) := [Pair(a, a)] + \\ + \\pairU64 : Pair(U64) + \\pairU64 = Pair.Pair(1, "Str") + ; + try assertFileTypeCheckFail(source, "INVALID NOMINAL TAG"); +} + +test "check type - nominal recursive type" { + const source = + \\module [] + \\ + \\ConsList(a) := [Nil, Cons(a, ConsList(a))] + \\ + \\x : ConsList(Str) + \\x = ConsList.Cons("hello", ConsList.Nil) + ; + try assertFileTypeCheckPass(source, "ConsList(Str)"); +} + +test "check type - nominal recursive type anno mismatch" { + const source = + \\module [] + \\ + \\ConsList(a) := [Nil, Cons(a, ConsList(a))] + \\ + \\x : ConsList(Num(size)) + \\x = ConsList.Cons("hello", ConsList.Nil) + ; + try assertFileTypeCheckFail(source, "TYPE MISMATCH"); +} + +test "check type - two nominal types" { + const source = + \\module [] + \\ + \\Elem(a) := [Elem(a)] + \\ + \\ConsList(a) := [Nil, Cons(a, ConsList(a))] + \\ + \\x = ConsList.Cons(Elem.Elem("hello"), ConsList.Nil) + ; + try assertFileTypeCheckPass(source, "ConsList(Elem(Str))"); +} + +test "check type - nominal recursive type no args" { + const source = + \\module [] + \\ + \\StrConsList := [Nil, Cons(Str, StrConsList)] + \\ + \\x : StrConsList + \\x = StrConsList.Cons("hello", StrConsList.Nil) + ; + try assertFileTypeCheckPass(source, "StrConsList"); +} + +test "check type - nominal recursive type wrong type" { + const source = + \\module [] + \\ + \\StrConsList := [Nil, Cons(Str, StrConsList)] + \\ + \\x : StrConsList + \\x = StrConsList.Cons(10, StrConsList.Nil) + ; + try assertFileTypeCheckFail(source, "INVALID NOMINAL TAG"); +} + +test "check type - nominal w/ polymorphic function with bad args" { + const source = + \\module [] + \\ + \\Pair(a) := [Pair(a, a)] + \\ + \\mkPairInvalid : a, b -> Pair(a) + \\mkPairInvalid = |x, y| Pair.Pair(x, y) + ; + try assertFileTypeCheckFail(source, "INVALID NOMINAL TAG"); +} + +test "check type - nominal w/ polymorphic function" { + const source = + \\module [] + \\ + \\Pair(a, b) : (a, b) + \\ + \\swapPair : Pair(a, b) -> Pair(b, a) + \\swapPair = |(x, y)| (y, x) + \\ + \\test = |_| swapPair((1, "test")) + ; + try assertFileTypeCheckPass(source, "_arg -> Pair(Str, Num(_size))"); +} + +// bool + +test "check type - bool unqualified" { + const source = + \\module [] + \\ + \\x = True + ; + try assertFileTypeCheckPass(source, "Bool"); +} + +test "check type - bool qualified" { + const source = + \\module [] + \\ + \\x = Bool.True + ; + try assertFileTypeCheckPass(source, "Bool"); +} + +test "check type - bool lambda" { + const source = + \\module [] + \\ + \\x = (|x| !x)(Bool.True) + ; + try assertFileTypeCheckPass(source, "Bool"); +} + +// if-else + +test "check type - if else" { + const source = + \\module [] + \\ + \\x : Str + \\x = if True "true" else "false" + ; + try assertFileTypeCheckPass(source, "Str"); +} + +test "check type - if else - qualified bool" { + const source = + \\module [] + \\ + \\x : Str + \\x = if Bool.True "true" else "false" + ; + try assertFileTypeCheckPass(source, "Str"); +} + +test "check type - if else - invalid condition 1" { + const source = + \\module [] + \\ + \\x : Str + \\x = if Truee "true" else "false" + ; + try assertFileTypeCheckFail(source, "INVALID IF CONDITION"); +} + +test "check type - if else - invalid condition 2" { + const source = + \\module [] + \\ + \\x : Str + \\x = if Bool.Falsee "true" else "false" + ; + try assertFileTypeCheckFail(source, "INVALID NOMINAL TAG"); +} + +test "check type - if else - invalid condition 3" { + const source = + \\module [] + \\ + \\x : Str + \\x = if "True" "true" else "false" + ; + try assertFileTypeCheckFail(source, "INVALID IF CONDITION"); +} + +test "check type - if else - different branch types 1" { + const source = + \\module [] + \\ + \\x = if True "true" else 10 + ; + try assertFileTypeCheckFail(source, "INCOMPATIBLE IF BRANCHES"); +} + +test "check type - if else - different branch types 2" { + const source = + \\module [] + \\ + \\x = if True "true" else if False "false" else 10 + ; + try assertFileTypeCheckFail(source, "INCOMPATIBLE IF BRANCHES"); +} + +test "check type - if else - different branch types 3" { + const source = + \\module [] + \\ + \\x = if True "true" else if False 10 else "last" + ; + try assertFileTypeCheckFail(source, "INCOMPATIBLE IF BRANCHES"); +} + +// match + +test "check type - match" { + const source = + \\module [] + \\ + \\x = + \\ match True { + \\ True => "true" + \\ False => "false" + \\ } + ; + try assertFileTypeCheckPass(source, "Str"); +} + +test "check type - match - diff cond types 1" { + const source = + \\module [] + \\ + \\x = + \\ match "hello" { + \\ True => "true" + \\ False => "false" + \\ } + ; + try assertFileTypeCheckFail(source, "INCOMPATIBLE MATCH PATTERNS"); +} + +test "check type - match - diff branch types" { + const source = + \\module [] + \\ + \\x = + \\ match True { + \\ True => "true" + \\ False => 100 + \\ } + ; + try assertFileTypeCheckFail(source, "INCOMPATIBLE MATCH BRANCHES"); +} + +// unary not + +test "check type - unary not" { + const source = + \\module [] + \\ + \\x = !True + ; + try assertFileTypeCheckPass(source, "Bool"); +} + +test "check type - unary not mismatch" { + const source = + \\module [] + \\ + \\x = !"Hello" + ; + try assertFileTypeCheckFail(source, "TYPE MISMATCH"); +} + +// unary not + +test "check type - unary minus" { + const source = + \\module [] + \\ + \\x = -10 + ; + try assertFileTypeCheckPass(source, "Num(_size)"); +} + +test "check type - unary minus mismatch" { + const source = + \\module [] + \\ + \\x = "hello" + \\ + \\y = -x + ; + try assertFileTypeCheckFail(source, "TYPE MISMATCH"); +} + +// binops + +test "check type - binops math plus" { + const source = + \\module [] + \\ + \\x = 10 + 10u32 + ; + try assertFileTypeCheckPass(source, "Num(Int(Unsigned32))"); +} + +test "check type - binops math sub" { + const source = + \\module [] + \\ + \\x = 1 - 0.2 + ; + try assertFileTypeCheckPass(source, "Num(Frac(_size))"); +} + +test "check type - binops ord" { + const source = + \\module [] + \\ + \\x = 10.0f32 > 15 + ; + try assertFileTypeCheckPass(source, "Bool"); +} + +test "check type - binops and" { + const source = + \\module [] + \\ + \\x = True and False + ; + try assertFileTypeCheckPass(source, "Bool"); +} + +test "check type - binops and mismatch" { + const source = + \\module [] + \\ + \\x = "Hello" and False + ; + try assertFileTypeCheckFail(source, "INVALID BOOL OPERATION"); +} + +test "check type - binops or" { + const source = + \\module [] + \\ + \\x = True or False + ; + try assertFileTypeCheckPass(source, "Bool"); +} + +test "check type - binops or mismatch" { + const source = + \\module [] + \\ + \\x = "Hello" or False + ; + try assertFileTypeCheckFail(source, "INVALID BOOL OPERATION"); +} + +// record access + +test "check type - record access" { + const source = + \\module [] + \\ + \\r = + \\ { + \\ hello: "Hello", + \\ world: 10, + \\ } + \\ + \\x = r.hello + ; + try assertFileTypeCheckPass(source, "Str"); +} + +test "check type - record access func polymorphic" { + const source = + \\module [] + \\ + \\x = |r| r.my_field + ; + try assertFileTypeCheckPass(source, "{ my_field: a } -> a"); +} + +test "check type - record access - not a record" { + const source = + \\module [] + \\ + \\r = "hello" + \\ + \\x = r.my_field + ; + try assertFileTypeCheckFail(source, "TYPE MISMATCH"); +} + +// tags // + +test "check type - patterns - wrong type" { + const source = + \\{ + \\ x = True + \\ + \\ match(x) { + \\ "hello" => "world", + \\ } + \\} + ; + try assertExprTypeCheckFail(source, "INCOMPATIBLE MATCH PATTERNS"); +} + +test "check type - patterns tag without payload" { + const source = + \\{ + \\ x = True + \\ + \\ match(x) { + \\ True => "true", + \\ False => "false", + \\ } + \\} + ; + try assertExprTypeCheckPass(source, "Str"); +} + +test "check type - patterns tag with payload" { + const source = + \\{ + \\ x = Ok("ok") + \\ + \\ match(x) { + \\ Ok(val) => val, + \\ Err(_) => "err", + \\ } + \\} + ; + try assertExprTypeCheckPass(source, "Str"); +} + +test "check type - patterns tag with payload mismatch" { + const source = + \\{ + \\ x = Ok("ok") + \\ + \\ match(x) { + \\ Ok(True) => 10 * 10, + \\ Err(_) => 0, + \\ } + \\} + ; + try assertExprTypeCheckFail(source, "INCOMPATIBLE MATCH PATTERNS"); +} + +test "check type - patterns str" { + const source = + \\{ + \\ x = "hello" + \\ + \\ match(x) { + \\ "world" => "true", + \\ _ => "false", + \\ } + \\} + ; + try assertExprTypeCheckPass(source, "Str"); +} + +test "check type - patterns num" { + const source = + \\{ + \\ x = 10 + \\ + \\ match(x) { + \\ 10 => "true", + \\ _ => "false", + \\ } + \\} + ; + try assertExprTypeCheckPass(source, "Str"); +} + +test "check type - patterns int mismatch" { + const source = + \\{ + \\ x = 10u8 + \\ + \\ match(x) { + \\ 10u32 => "true", + \\ _ => "false", + \\ } + \\} + ; + try assertExprTypeCheckFail(source, "INCOMPATIBLE MATCH PATTERNS"); +} + +test "check type - patterns frac 1" { + const source = + \\{ + \\ x = 10.0dec + \\ + \\ match(x) { + \\ 10 => x, + \\ _ => 15, + \\ } + \\} + ; + try assertExprTypeCheckPass(source, "Num(Frac(Decimal))"); +} + +test "check type - patterns frac 2" { + const source = + \\{ + \\ x = 10.0 + \\ + \\ match(x) { + \\ 10f32 => x, + \\ _ => 15, + \\ } + \\} + ; + try assertExprTypeCheckPass(source, "Num(Frac(Float32))"); +} + +test "check type - patterns frac 3" { + const source = + \\{ + \\ x = 10.0 + \\ + \\ match(x) { + \\ 10 => x, + \\ 15f64 => x, + \\ _ => 20, + \\ } + \\} + ; + try assertExprTypeCheckPass(source, "Num(Frac(Float64))"); +} + +test "check type - patterns list" { + const source = + \\{ + \\ x = ["a", "b", "c"] + \\ + \\ match(x) { + \\ [.. as b, a] => b, + \\ [a, .. as b] => b, + \\ [] => [], + \\ } + \\} + ; + try assertExprTypeCheckPass(source, "List(Str)"); +} + +test "check type - patterns record" { + const source = + \\{ + \\ val = { x: "hello", y: True } + \\ + \\ match(val) { + \\ { y: False } => "False", + \\ { x } => x, + \\ } + \\} + ; + try assertExprTypeCheckPass(source, "Str"); +} + +test "check type - patterns record 2" { + const source = + \\{ + \\ val = { x: "hello", y: True } + \\ + \\ match(val) { + \\ { y: False, x: "world" } => 10 + \\ _ => 20, + \\ } + \\} + ; + try assertExprTypeCheckPass(source, "Num(_size)"); +} + +test "check type - patterns record field mismatch" { + const source = + \\{ + \\ val = { x: "hello" } + \\ + \\ match(val) { + \\ { x: False } => 10 + \\ _ => 20 + \\ } + \\} + ; + try assertExprTypeCheckFail(source, "INCOMPATIBLE MATCH PATTERNS"); +} + +// vars + +test "check type - var ressignment" { + const source = + \\module [] + \\ + \\main = { + \\ var x = 1 + \\ x = x + 1 + \\ x + \\} + ; + try assertFileTypeCheckPass(source, "Num(_size)"); +} + +// expect + +test "check type - expect" { + const source = + \\module [] + \\ + \\main = { + \\ x = 1 + \\ expect x == 1 + \\ x + \\} + ; + try assertFileTypeCheckPass(source, "Num(_size)"); +} + +test "check type - expect not bool" { + const source = + \\module [] + \\ + \\main = { + \\ x = 1 + \\ expect x + \\ x + \\} + ; + try assertFileTypeCheckFail(source, "TYPE MISMATCH"); +} + +// helpers // + +/// A unified helper to run the full pipeline: parse, canonicalize, and type-check source code. +/// Asserts that type checking the expr passes +fn assertExprTypeCheckPass(comptime source_expr: []const u8, expected_type: []const u8) !void { + var test_env = try TestEnv.initExpr(source_expr); + defer test_env.deinit(); + return test_env.assertLastDefType(expected_type); +} + +/// A unified helper to run the full pipeline: parse, canonicalize, and type-check source code. +/// Asserts that type checking the expr fails with exactly one problem, and the title of the problem matches the provided one. +fn assertExprTypeCheckFail(comptime source_expr: []const u8, expected_problem_title: []const u8) !void { + var test_env = try TestEnv.initExpr(source_expr); + defer test_env.deinit(); + return test_env.assertOneTypeError(expected_problem_title); +} + +/// A unified helper to run the full pipeline: parse, canonicalize, and type-check source code. +/// Asserts that the type of the final definition in the source matches the one provided +fn assertFileTypeCheckPass(source: []const u8, expected_type: []const u8) !void { + var test_env = try TestEnv.init(source); + defer test_env.deinit(); + return test_env.assertLastDefType(expected_type); +} + +/// A unified helper to run the full pipeline: parse, canonicalize, and type-check source code. +/// Asserts that the type of the final definition in the source matches the one provided +fn assertFileTypeCheckFail(source: []const u8, expected_problem_title: []const u8) !void { + var test_env = try TestEnv.init(source); + defer test_env.deinit(); + return test_env.assertOneTypeError(expected_problem_title); +} diff --git a/src/check/test/unify_test.zig b/src/check/test/unify_test.zig new file mode 100644 index 0000000000..34f9687169 --- /dev/null +++ b/src/check/test/unify_test.zig @@ -0,0 +1,3474 @@ +//! 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 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 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; +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, + 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 unify_mod.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, + } + } + + /// 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 - nums // + + fn mkNumPoly(self: *Self, var_: Var) std.mem.Allocator.Error!Var { + return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = var_ } } }); + } + + fn mkNumPolyFlex(self: *Self) std.mem.Allocator.Error!Var { + const flex_var = try self.module_env.types.freshFromContent(.{ .flex = Flex.init() }); + return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = flex_var } } }); + } + + fn mkNumPolyRigid(self: *Self, name: []const u8) std.mem.Allocator.Error!Var { + const rigid_var = try self.module_env.types.freshFromContent(try self.mkRigidVar(name)); + return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = rigid_var } } }); + } + + // helpers - nums - ints // + + fn mkIntConcrete(self: *Self, var_: Var) std.mem.Allocator.Error!Var { + const int_var = try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_poly = var_ } } }); + return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = int_var } } }); + } + + fn mkIntPolyFlex(self: *Self) std.mem.Allocator.Error!Var { + const flex_var = try self.module_env.types.freshFromContent(.{ .flex = Flex.init() }); + const int_var = try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_poly = flex_var } } }); + return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = int_var } } }); + } + + fn mkIntPolyRigid(self: *Self, name: []const u8) std.mem.Allocator.Error!Var { + const rigid_var = try self.module_env.types.freshFromContent(try self.mkRigidVar(name)); + const int_var = try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_poly = rigid_var } } }); + return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = int_var } } }); + } + + 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_var = try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_poly = prec_var } } }); + return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = int_var } } }); + } + + // helpers - nums - fracs // + + fn mkFracConcrete(self: *Self, var_: Var) std.mem.Allocator.Error!Var { + const int_var = try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_poly = var_ } } }); + return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = int_var } } }); + } + + fn mkFracPolyFlex(self: *Self) std.mem.Allocator.Error!Var { + const flex_var = try self.module_env.types.freshFromContent(.{ .flex = Flex.init() }); + const frac_var = try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_poly = flex_var } } }); + return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = frac_var } } }); + } + + fn mkFracPolyRigid(self: *Self, name: []const u8) std.mem.Allocator.Error!Var { + const rigid_var = try self.module_env.types.freshFromContent(try self.mkRigidVar(name)); + const frac_var = try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_poly = rigid_var } } }); + return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = frac_var } } }); + } + + 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_var = try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_poly = prec_var } } }); + return try self.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = frac_var } } }); + } + + // 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 = 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)); +} + +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 = 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 = .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); +} + +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 = .str }); + 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 = .str }); + + 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 = .str }, 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 = .str }); + const b_alias = try env.mkAlias("Alias", b_backing_var, &[_]Var{}); + + const a = try env.module_env.types.freshFromContent(Content{ .structure = .str }); + 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 = .str }, 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 = .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.mkNumPolyFlex(); + 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.mkIntPolyFlex(); + 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.mkIntPoly(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.mkIntPoly(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.mkNumPolyFlex(); + 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.mkFracPolyFlex(); + 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.mkFracPoly(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.mkFracPoly(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.mkNumPolyFlex(); + + 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.mkIntPolyFlex(); + + 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.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)); + 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.mkIntPoly(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.mkNumPolyFlex(); + + 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.mkFracPolyFlex(); + + 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.mkFracPoly(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.mkFracPoly(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 num = Content{ .structure = .{ .num = .{ .num_poly = rigid } } }; + 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 a = try env.mkNumPolyRigid("a"); + const b = try env.mkNumPolyRigid("b"); + + 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 num = try env.mkIntPolyRigid("a"); + const a = num; + const b = num; + + const result = try env.unify(a, b); + + try std.testing.expectEqual(true, result.isOk()); + try std.testing.expectEqual((try env.getDescForRootVar(num)).content, (try env.getDescForRootVar(a)).content); + try std.testing.expectEqual((try env.getDescForRootVar(num)).content, (try env.getDescForRootVar(b)).content); +} + +test "unify - Num(Int(rigid_a)) and Num(Int(rigid_b))" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const a = try env.mkIntPolyRigid("a"); + const b = try env.mkIntPolyRigid("b"); + + 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 Num(Frac(rigid))" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const num = try env.mkFracPolyRigid("b"); + const a = num; + const b = num; + + const result = try env.unify(a, b); + + try std.testing.expectEqual(true, result.isOk()); + try std.testing.expectEqual((try env.getDescForRootVar(num)).content, (try env.getDescForRootVar(a)).content); + try std.testing.expectEqual((try env.getDescForRootVar(num)).content, (try env.getDescForRootVar(b)).content); +} + +test "unify - Num(Frac(rigid_a)) and Num(Frac(rigid_b))" { + const gpa = std.testing.allocator; + + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const a = try env.mkFracPolyRigid("a"); + const b = try env.mkFracPolyRigid("b"); + + 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/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.mkFracPolyRigid("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.mkFracPolyRigid("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.mkFracPolyRigid("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.mkFracPolyRigid("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 = try env.mkNumPolyFlex(); + 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 = try env.mkIntPolyFlex(); + 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 = try env.mkIntPolyFlex(); + 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 = try env.mkIntPolyFlex(); + 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 (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 = try env.mkIntPolyFlex(); + 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()); + 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 unbound" { + 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 = try env.mkIntPolyFlex(); + const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); + const pure_func = try env.mkFuncPure(&[_]Var{ str, int_poly }, int_i32); + const unbound_func = try env.mkFuncUnbound(&[_]Var{ str, int_poly }, int_i32); + + const a = try env.module_env.types.freshFromContent(pure_func); + const b = try env.module_env.types.freshFromContent(unbound_func); + + 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(pure_func, (try env.getDescForRootVar(b)).content); +} + +test "unify - same funcs first unbound, second 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 = try env.mkIntPolyFlex(); + const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); + const pure_func = try env.mkFuncPure(&[_]Var{ str, int_poly }, int_i32); + const unbound_func = try env.mkFuncUnbound(&[_]Var{ str, int_poly }, int_i32); + + const a = try env.module_env.types.freshFromContent(unbound_func); + const b = try env.module_env.types.freshFromContent(pure_func); + + 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(pure_func, (try env.getDescForRootVar(b)).content); +} + +test "unify - same funcs first effectful, second unbound" { + 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 = try env.mkIntPolyFlex(); + const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); + const eff_func = try env.mkFuncEffectful(&[_]Var{ str, int_poly }, int_i32); + const unbound_func = try env.mkFuncUnbound(&[_]Var{ str, int_poly }, int_i32); + + const a = try env.module_env.types.freshFromContent(eff_func); + const b = try env.module_env.types.freshFromContent(unbound_func); + + 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(eff_func, (try env.getDescForRootVar(b)).content); +} + +test "unify - same funcs first unbound, second 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 = try env.mkIntPolyFlex(); + const str = try env.module_env.types.freshFromContent(Content{ .structure = .str }); + const eff_func = try env.mkFuncEffectful(&[_]Var{ str, int_poly }, int_i32); + const unbound_func = try env.mkFuncUnbound(&[_]Var{ str, int_poly }, int_i32); + + const a = try env.module_env.types.freshFromContent(unbound_func); + const b = try env.module_env.types.freshFromContent(eff_func); + + 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(eff_func, (try env.getDescForRootVar(b)).content); +} + +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 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 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 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 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 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 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 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 = .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 = Flex.init() }, 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 = Flex.init() }, 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 = Flex.init() }, 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 = Flex.init() }, 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 = Flex.init() }, 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 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 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 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 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 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 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 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 = .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 = Flex.init() }, 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 = Flex.init() }, 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 = Flex.init() }, 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 = Flex.init() }, 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 = Flex.init() }, 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.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.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); +} + +// numbers // +// +// Numbers are fairly complicated in roc. We have: +// * Polymorphic nums +// * Compacted nums +// * Unbounded nums +// +// Most type systems only have polymorphic numbers, but we have concepts of +// * Comapcted nums, which are fully concrete numbers +// * Unbound nums, which are polymorphic flex var numbers +// +// To the user though, they just see regular numbers like U8 or I64. +// +// Each of these have to be able to unify with each other. So we have +// sophisticated test infra to test various combinations of these. + +/// Specification for an unbound number type (literal) +const NumTypeUnbound = union(enum) { + /// Just int requirements (will be wrapped in Num(Int(_))) + int: Num.IntRequirements, + /// Just frac requirements (will be wrapped in Num(Frac(_))) + frac: Num.FracRequirements, + /// Both int and frac requirements (Num(_)) + num: Num.NumRequirements, + + /// Helper constructors + pub fn intLiteral(value: u128, is_negative: bool) NumTypeUnbound { + return .{ + .int = Num.IntRequirements.fromIntLiteral(value, is_negative), + }; + } + + pub fn fracLiteral(fits_f32: bool, fits_dec: bool) NumTypeUnbound { + return .{ .frac = .{ .fits_in_f32 = fits_f32, .fits_in_dec = fits_dec } }; + } + + pub fn numLiteral(int_value: u128, is_negative: bool, fits_f32: bool, fits_dec: bool) NumTypeUnbound { + return .{ .num = .{ + .int_requirements = Num.IntRequirements.fromIntLiteral(int_value, is_negative), + .frac_requirements = .{ .fits_in_f32 = fits_f32, .fits_in_dec = fits_dec }, + } }; + } +}; + +/// Specification for a concrete/polymorphic number type +const NumTypeBound = enum { + // Concrete integer types (these are Num(Int(U8)), etc.) + u8, + i8, + u16, + i16, + u32, + i32, + u64, + i64, + u128, + i128, + + // Concrete float types (these are Num(Frac(Dec)), etc.) + f32, + f64, + dec, + + // Polymorphic types + num_poly_flex, // Num(flex_var) + int_poly_flex, // Num(Int(flex_var)) + frac_poly_flex, // Num(Frac(flex_var)) +}; + +const NumTestCase = struct { + env: *TestEnv, + a_spec: NumSpec, + b_spec: NumSpec, + + const Self = @This(); + + const NumSpec = union(enum) { + unbound: NumTypeUnbound, + bound: NumTypeBound, + }; + + /// Initialize with an unbound literal and a bound type + fn init(env: *TestEnv, unbound: NumTypeUnbound, bound: NumTypeBound) !Self { + return .{ + .env = env, + .a_spec = .{ .unbound = unbound }, + .b_spec = .{ .bound = bound }, + }; + } + + /// Initialize with two bound types (for polymorphic × polymorphic tests) + fn initBothBound(env: *TestEnv, a: NumTypeBound, b: NumTypeBound) !Self { + return .{ + .env = env, + .a_spec = .{ .bound = a }, + .b_spec = .{ .bound = b }, + }; + } + + /// Initialize with two unbound types (for requirement merging tests) + fn initBothUnbound(env: *TestEnv, a: NumTypeUnbound, b: NumTypeUnbound) !Self { + return .{ + .env = env, + .a_spec = .{ .unbound = a }, + .b_spec = .{ .unbound = b }, + }; + } + + /// Create fresh variables from the specs + fn makeVars(self: Self) !struct { a: Var, b: Var } { + const a = switch (self.a_spec) { + .unbound => |spec| try makeUnboundVar(self.env, spec), + .bound => |spec| try makeBoundVar(self.env, spec), + }; + const b = switch (self.b_spec) { + .unbound => |spec| try makeUnboundVar(self.env, spec), + .bound => |spec| try makeBoundVar(self.env, spec), + }; + return .{ .a = a, .b = b }; + } + + fn expectOk(self: Self) !void { + const vars = try self.makeVars(); + const result = try self.env.unify(vars.a, vars.b); + try std.testing.expect(result == .ok); + } + + fn expectProblem(self: Self) !void { + const vars = try self.makeVars(); + const result = try self.env.unify(vars.a, vars.b); + try std.testing.expect(result == .problem); + } + + fn expectBothOrders(self: Self, comptime expectFn: fn (Self) anyerror!void) !void { + // Test a → b + try expectFn(self); + + // Test b → a (swap the specs, not the vars) + const swapped = Self{ + .env = self.env, + .a_spec = self.b_spec, + .b_spec = self.a_spec, + }; + try expectFn(swapped); + } + + /// For tests that need to inspect the unified result + fn unifyAndGetResult(self: Self) !struct { result: unify_mod.Result, a: Var, b: Var } { + const vars = try self.makeVars(); + const result = try self.env.unify(vars.a, vars.b); + return .{ .result = result, .a = vars.a, .b = vars.b }; + } + + fn makeUnboundVar(env: *TestEnv, spec: NumTypeUnbound) !Var { + return switch (spec) { + // int_unbound needs to be wrapped: Num(Int(unbound)) + .int => |reqs| blk: { + const int_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_unbound = reqs } } }); + break :blk try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = int_var } } }); + }, + // frac_unbound needs to be wrapped: Num(Frac(unbound)) + .frac => |reqs| blk: { + const frac_var = try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .frac_unbound = reqs } } }); + break :blk try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_poly = frac_var } } }); + }, + // num_unbound is already at the top level: Num(unbound) + .num => |reqs| try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = .{ .num_unbound = reqs } } }), + }; + } + + fn makeBoundVar(env: *TestEnv, spec: NumTypeBound) !Var { + return switch (spec) { + // These are already num_compact which is the full Num(Int(U8)) structure + .u8 => try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u8 } }), + .i8 => try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i8 } }), + .u16 => try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u16 } }), + .i16 => try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i16 } }), + .u32 => try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u32 } }), + .i32 => try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i32 } }), + .u64 => try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u64 } }), + .i64 => try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i64 } }), + .u128 => try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_u128 } }), + .i128 => try env.module_env.types.freshFromContent(Content{ .structure = .{ .num = Num.int_i128 } }), + + // These use mkFracPoly which builds: Num(Frac(precision)) + .f32 => try env.mkFracPoly(Num.Frac.Precision.f32), + .f64 => try env.mkFracPoly(Num.Frac.Precision.f64), + .dec => try env.mkFracPoly(Num.Frac.Precision.dec), + + // These are already properly wrapped by the mk functions + .num_poly_flex => try env.mkNumPolyFlex(), // Num(flex_var) + .int_poly_flex => try env.mkIntPolyFlex(), // Num(Int(flex_var)) + .frac_poly_flex => try env.mkFracPolyFlex(), // Num(Frac(flex_var)) + }; + } +}; + +// num basic test cases // + +test "unify - 255 fits in U8" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const case = try NumTestCase.init(&env, NumTypeUnbound.intLiteral(255, false), .u8); + try case.expectBothOrders(NumTestCase.expectOk); +} + +test "unify - 256 does not fit in U8" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const case = try NumTestCase.init(&env, NumTypeUnbound.intLiteral(256, false), .u8); + try case.expectBothOrders(NumTestCase.expectProblem); +} + +test "unify - 127 fits in I8" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const case = try NumTestCase.init(&env, NumTypeUnbound.intLiteral(127, false), .i8); + try case.expectBothOrders(NumTestCase.expectOk); +} + +test "unify - 128 does not fit in I8" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const case = try NumTestCase.init(&env, NumTypeUnbound.intLiteral(128, false), .i8); + try case.expectBothOrders(NumTestCase.expectProblem); +} + +test "unify - -128 fits in I8" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const case = try NumTestCase.init(&env, NumTypeUnbound.intLiteral(128, true), .i8); + try case.expectBothOrders(NumTestCase.expectOk); +} + +test "unify - -129 does not fit in I8" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const case = try NumTestCase.init(&env, NumTypeUnbound.intLiteral(129, true), .i8); + try case.expectBothOrders(NumTestCase.expectProblem); +} + +test "unify - two unbound nums merge requirements" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const case = try NumTestCase.initBothUnbound( + &env, + NumTypeUnbound.numLiteral(256, false, true, false), + NumTypeUnbound.numLiteral(100, true, false, true), + ); + + const unified = try case.unifyAndGetResult(); + try std.testing.expect(unified.result == .ok); + + // Verify merged requirements on the unified type + const resolved = env.module_env.types.resolveVar(unified.a).desc.content.structure.num.num_unbound; + try std.testing.expectEqual(true, resolved.int_requirements.sign_needed); + try std.testing.expectEqual((Num.Int.BitsNeeded.@"9_to_15").toBits(), resolved.int_requirements.bits_needed); + try std.testing.expectEqual(false, resolved.frac_requirements.fits_in_f32); + try std.testing.expectEqual(false, resolved.frac_requirements.fits_in_dec); +} + +test "unify - int_poly_flex unifies with num_unbound" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const case = try NumTestCase.initBothBound(&env, .int_poly_flex, .num_poly_flex); + try case.expectBothOrders(NumTestCase.expectOk); +} + +test "unify - literal vs concrete - exhaustive boundary tests" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const TestCase = struct { + value: u128, + is_negative: bool, + target: NumTypeBound, + should_succeed: bool, + }; + + const cases = [_]TestCase{ + // U8 boundaries (0-255) + .{ .value = 0, .is_negative = false, .target = .u8, .should_succeed = true }, + .{ .value = 127, .is_negative = false, .target = .u8, .should_succeed = true }, + .{ .value = 255, .is_negative = false, .target = .u8, .should_succeed = true }, + .{ .value = 256, .is_negative = false, .target = .u8, .should_succeed = false }, + .{ .value = 1, .is_negative = true, .target = .u8, .should_succeed = false }, + + // I8 boundaries (-128 to 127) + .{ .value = 0, .is_negative = false, .target = .i8, .should_succeed = true }, + .{ .value = 127, .is_negative = false, .target = .i8, .should_succeed = true }, + .{ .value = 128, .is_negative = false, .target = .i8, .should_succeed = false }, + .{ .value = 1, .is_negative = true, .target = .i8, .should_succeed = true }, + .{ .value = 128, .is_negative = true, .target = .i8, .should_succeed = true }, // -128 + .{ .value = 129, .is_negative = true, .target = .i8, .should_succeed = false }, + + // U16 boundaries (0-65535) + .{ .value = 255, .is_negative = false, .target = .u16, .should_succeed = true }, + .{ .value = 256, .is_negative = false, .target = .u16, .should_succeed = true }, + .{ .value = 65535, .is_negative = false, .target = .u16, .should_succeed = true }, + .{ .value = 65536, .is_negative = false, .target = .u16, .should_succeed = false }, + .{ .value = 1, .is_negative = true, .target = .u16, .should_succeed = false }, + + // I16 boundaries (-32768 to 32767) + .{ .value = 32767, .is_negative = false, .target = .i16, .should_succeed = true }, + .{ .value = 32768, .is_negative = false, .target = .i16, .should_succeed = false }, + .{ .value = 32768, .is_negative = true, .target = .i16, .should_succeed = true }, // -32768 + .{ .value = 32769, .is_negative = true, .target = .i16, .should_succeed = false }, + + // U32 boundaries + .{ .value = 65535, .is_negative = false, .target = .u32, .should_succeed = true }, + .{ .value = 65536, .is_negative = false, .target = .u32, .should_succeed = true }, + .{ .value = 4294967295, .is_negative = false, .target = .u32, .should_succeed = true }, + .{ .value = 4294967296, .is_negative = false, .target = .u32, .should_succeed = false }, + + // I32 boundaries (-2147483648 to 2147483647) + .{ .value = 2147483647, .is_negative = false, .target = .i32, .should_succeed = true }, + .{ .value = 2147483648, .is_negative = false, .target = .i32, .should_succeed = false }, + .{ .value = 2147483648, .is_negative = true, .target = .i32, .should_succeed = true }, + .{ .value = 2147483649, .is_negative = true, .target = .i32, .should_succeed = false }, + + // U64 boundaries + .{ .value = 4294967295, .is_negative = false, .target = .u64, .should_succeed = true }, + .{ .value = 18446744073709551615, .is_negative = false, .target = .u64, .should_succeed = true }, + + // I64 boundaries + .{ .value = 9223372036854775807, .is_negative = false, .target = .i64, .should_succeed = true }, + .{ .value = 9223372036854775808, .is_negative = false, .target = .i64, .should_succeed = false }, + .{ .value = 9223372036854775808, .is_negative = true, .target = .i64, .should_succeed = true }, + + // U128 - always succeeds for positive + .{ .value = 18446744073709551615, .is_negative = false, .target = .u128, .should_succeed = true }, + .{ .value = 1, .is_negative = true, .target = .u128, .should_succeed = false }, + + // I128 - succeeds for all values we can represent + .{ .value = 18446744073709551615, .is_negative = false, .target = .i128, .should_succeed = true }, + .{ .value = 18446744073709551615, .is_negative = true, .target = .i128, .should_succeed = true }, + }; + + for (cases) |tc| { + const case = try NumTestCase.init(&env, NumTypeUnbound.intLiteral(tc.value, tc.is_negative), tc.target); + + if (tc.should_succeed) { + try case.expectBothOrders(NumTestCase.expectOk); + } else { + try case.expectBothOrders(NumTestCase.expectProblem); + } + } +} + +test "unify - frac literal vs concrete - boundary tests" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const TestCase = struct { + fits_f32: bool, + fits_dec: bool, + target: NumTypeBound, + should_succeed: bool, + }; + + const cases = [_]TestCase{ + // Fits in both + .{ .fits_f32 = true, .fits_dec = true, .target = .f32, .should_succeed = true }, + .{ .fits_f32 = true, .fits_dec = true, .target = .f64, .should_succeed = true }, + .{ .fits_f32 = true, .fits_dec = true, .target = .dec, .should_succeed = true }, + + // Only fits in f32 (and therefore f64) + .{ .fits_f32 = true, .fits_dec = false, .target = .f32, .should_succeed = true }, + .{ .fits_f32 = true, .fits_dec = false, .target = .f64, .should_succeed = true }, + .{ .fits_f32 = true, .fits_dec = false, .target = .dec, .should_succeed = false }, + + // Only fits in dec + .{ .fits_f32 = false, .fits_dec = true, .target = .f32, .should_succeed = false }, + .{ .fits_f32 = false, .fits_dec = true, .target = .f64, .should_succeed = true }, + .{ .fits_f32 = false, .fits_dec = true, .target = .dec, .should_succeed = true }, + + // Only fits in f64 + .{ .fits_f32 = false, .fits_dec = false, .target = .f32, .should_succeed = false }, + .{ .fits_f32 = false, .fits_dec = false, .target = .f64, .should_succeed = true }, + .{ .fits_f32 = false, .fits_dec = false, .target = .dec, .should_succeed = false }, + }; + + for (cases) |tc| { + const case = try NumTestCase.init(&env, NumTypeUnbound.fracLiteral(tc.fits_f32, tc.fits_dec), tc.target); + + if (tc.should_succeed) { + try case.expectBothOrders(NumTestCase.expectOk); + } else { + try case.expectBothOrders(NumTestCase.expectProblem); + } + } +} + +test "unify - unbound vs polymorphic - full matrix" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const UnboundKind = enum { int, frac, num }; + const PolyKind = enum { int_poly_flex, frac_poly_flex, num_poly_flex }; + + // Matrix: should unbound × poly unify? + const ShouldUnify = enum { yes, no }; + const should_unify = std.EnumArray(UnboundKind, std.EnumArray(PolyKind, ShouldUnify)).init(.{ + .int = std.EnumArray(PolyKind, ShouldUnify).init(.{ + .int_poly_flex = .yes, // Num(Int(a)) × Num(Int(b)) → yes + .frac_poly_flex = .no, // Num(Int(a)) × Num(Frac(b)) → NO! + .num_poly_flex = .yes, // Num(Int(a)) × Num(b) → yes + }), + .frac = std.EnumArray(PolyKind, ShouldUnify).init(.{ + .int_poly_flex = .no, // Num(Frac(a)) × Num(Int(b)) → NO! + .frac_poly_flex = .yes, // Num(Frac(a)) × Num(Frac(b)) → yes + .num_poly_flex = .yes, // Num(Frac(a)) × Num(b) → yes + }), + .num = std.EnumArray(PolyKind, ShouldUnify).init(.{ + .int_poly_flex = .yes, // Num(a) × Num(Int(b)) → yes + .frac_poly_flex = .yes, // Num(a) × Num(Frac(b)) → yes + .num_poly_flex = .yes, // Num(a) × Num(b) → yes + }), + }); + + const unbound_specs = std.EnumArray(UnboundKind, NumTypeUnbound).init(.{ + .int = NumTypeUnbound.intLiteral(100, false), + .frac = NumTypeUnbound.fracLiteral(true, true), + .num = NumTypeUnbound.numLiteral(100, false, true, true), + }); + + const poly_specs = std.EnumArray(PolyKind, NumTypeBound).init(.{ + .int_poly_flex = .int_poly_flex, + .frac_poly_flex = .frac_poly_flex, + .num_poly_flex = .num_poly_flex, + }); + + inline for (std.meta.fields(UnboundKind)) |unbound_field| { + inline for (std.meta.fields(PolyKind)) |poly_field| { + const unbound_kind = @field(UnboundKind, unbound_field.name); + const poly_kind = @field(PolyKind, poly_field.name); + + const case = try NumTestCase.init(&env, unbound_specs.get(unbound_kind), poly_specs.get(poly_kind)); + + const expected = should_unify.get(unbound_kind).get(poly_kind); + + // Add context on failure + if (expected == .yes) { + case.expectBothOrders(NumTestCase.expectOk) catch |err| { + std.debug.print("FAILED: {s} × {s} (expected to unify)\n", .{ @tagName(unbound_kind), @tagName(poly_kind) }); + return err; + }; + } else { + case.expectBothOrders(NumTestCase.expectProblem) catch |err| { + std.debug.print("FAILED: {s} × {s} (expected NOT to unify)\n", .{ @tagName(unbound_kind), @tagName(poly_kind) }); + return err; + }; + } + } + } +} + +test "unify - polymorphic vs polymorphic - full matrix" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const PolyKind = enum { num_poly_flex, int_poly_flex, frac_poly_flex }; + + // Matrix: should poly × poly unify? + const ShouldUnify = enum { yes, no }; + const should_unify = std.EnumArray(PolyKind, std.EnumArray(PolyKind, ShouldUnify)).init(.{ + .num_poly_flex = std.EnumArray(PolyKind, ShouldUnify).init(.{ + .num_poly_flex = .yes, // Num(a) × Num(b) → yes + .int_poly_flex = .yes, // Num(a) × Num(Int(b)) → yes + .frac_poly_flex = .yes, // Num(a) × Num(Frac(b)) → yes + }), + .int_poly_flex = std.EnumArray(PolyKind, ShouldUnify).init(.{ + .num_poly_flex = .yes, // Num(Int(a)) × Num(b) → yes + .int_poly_flex = .yes, // Num(Int(a)) × Num(Int(b)) → yes + .frac_poly_flex = .no, // Num(Int(a)) × Num(Frac(b)) → NO! + }), + .frac_poly_flex = std.EnumArray(PolyKind, ShouldUnify).init(.{ + .num_poly_flex = .yes, // Num(Frac(a)) × Num(b) → yes + .int_poly_flex = .no, // Num(Frac(a)) × Num(Int(b)) → NO! + .frac_poly_flex = .yes, // Num(Frac(a)) × Num(Frac(b)) → yes + }), + }); + + const poly_specs = std.EnumArray(PolyKind, NumTypeBound).init(.{ + .num_poly_flex = .num_poly_flex, + .int_poly_flex = .int_poly_flex, + .frac_poly_flex = .frac_poly_flex, + }); + + inline for (std.meta.fields(PolyKind)) |a_field| { + inline for (std.meta.fields(PolyKind)) |b_field| { + const a_kind = @field(PolyKind, a_field.name); + const b_kind = @field(PolyKind, b_field.name); + + const case = try NumTestCase.initBothBound(&env, poly_specs.get(a_kind), poly_specs.get(b_kind)); + + const expected = should_unify.get(a_kind).get(b_kind); + + if (expected == .yes) { + case.expectBothOrders(NumTestCase.expectOk) catch |err| { + std.debug.print("FAILED: {s} × {s} (expected to unify)\n", .{ @tagName(a_kind), @tagName(b_kind) }); + return err; + }; + } else { + case.expectBothOrders(NumTestCase.expectProblem) catch |err| { + std.debug.print("FAILED: {s} × {s} (expected NOT to unify)\n", .{ @tagName(a_kind), @tagName(b_kind) }); + return err; + }; + } + } + } +} + +test "unify - unbound vs unbound - requirement merging" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Test that int requirements are merged correctly + { + const case = try NumTestCase.initBothUnbound(&env, NumTypeUnbound.intLiteral(256, false), // needs 9 bits, unsigned + NumTypeUnbound.intLiteral(100, true) // needs 7 bits, signed + ); + + const unified = try case.unifyAndGetResult(); + try std.testing.expect(unified.result == .ok); + + // Should take max bits (9) and OR the sign (true) + const resolved = env.module_env.types.resolveVar(unified.a).desc.content; + const int_poly = resolved.structure.num.num_poly; + const int_unbound = env.module_env.types.resolveVar(int_poly).desc.content.structure.num.int_unbound; + + try std.testing.expectEqual(true, int_unbound.sign_needed); + try std.testing.expectEqual(Num.Int.BitsNeeded.@"9_to_15".toBits(), int_unbound.bits_needed); + } + + // Test that frac requirements are merged correctly + { + const case = try NumTestCase.initBothUnbound(&env, NumTypeUnbound.fracLiteral(true, false), // fits f32 only + NumTypeUnbound.fracLiteral(false, true) // fits dec only + ); + + const unified = try case.unifyAndGetResult(); + try std.testing.expect(unified.result == .ok); + + // Should take AND of capabilities (both false) + const resolved = env.module_env.types.resolveVar(unified.a).desc.content; + const frac_poly = resolved.structure.num.num_poly; + const frac_unbound = env.module_env.types.resolveVar(frac_poly).desc.content.structure.num.frac_unbound; + + try std.testing.expectEqual(false, frac_unbound.fits_in_f32); + try std.testing.expectEqual(false, frac_unbound.fits_in_dec); + } + + // Test that num requirements merge both int and frac (Num(num_unbound)) + { + const case = try NumTestCase.initBothUnbound(&env, NumTypeUnbound.numLiteral(256, false, true, false), NumTypeUnbound.numLiteral(100, true, false, true)); + + const unified = try case.unifyAndGetResult(); + try std.testing.expect(unified.result == .ok); + + const resolved = env.module_env.types.resolveVar(unified.a).desc.content.structure.num.num_unbound; + + try std.testing.expectEqual(true, resolved.int_requirements.sign_needed); + try std.testing.expectEqual(Num.Int.BitsNeeded.@"9_to_15".toBits(), resolved.int_requirements.bits_needed); + try std.testing.expectEqual(false, resolved.frac_requirements.fits_in_f32); + try std.testing.expectEqual(false, resolved.frac_requirements.fits_in_dec); + } + + // Test that Num(int_unbound) × Num(frac_unbound) doesn't unify + { + const case = try NumTestCase.initBothUnbound(&env, NumTypeUnbound.intLiteral(100, false), NumTypeUnbound.fracLiteral(true, true)); + + try case.expectProblem(); + } + + // Test that Num(num_unbound) × Num(Int(int_unbound)) unifies and merges int requirements + { + const case = try NumTestCase.initBothUnbound(&env, NumTypeUnbound.numLiteral(256, false, true, false), // Num(num_unbound) + NumTypeUnbound.intLiteral(100, true) // Num(Int(int_unbound)) + ); + + const unified = try case.unifyAndGetResult(); + try std.testing.expect(unified.result == .ok); + + // Result should be Num(Int(int_unbound)) with merged requirements + const resolved = env.module_env.types.resolveVar(unified.a).desc.content; + const int_poly = resolved.structure.num.num_poly; + const int_unbound = env.module_env.types.resolveVar(int_poly).desc.content.structure.num.int_unbound; + + try std.testing.expectEqual(true, int_unbound.sign_needed); + try std.testing.expectEqual(Num.Int.BitsNeeded.@"9_to_15".toBits(), int_unbound.bits_needed); + } + + // Test that Num(num_unbound) × Num(Frac(frac_unbound)) unifies and merges frac requirements + { + const case = try NumTestCase.initBothUnbound(&env, NumTypeUnbound.numLiteral(100, false, true, false), // Num(num_unbound) + NumTypeUnbound.fracLiteral(false, true) // Num(Frac(frac_unbound)) + ); + + const unified = try case.unifyAndGetResult(); + try std.testing.expect(unified.result == .ok); + + // Result should be Num(Frac(frac_unbound)) with merged requirements + const resolved = env.module_env.types.resolveVar(unified.a).desc.content; + const frac_poly = resolved.structure.num.num_poly; + const frac_unbound = env.module_env.types.resolveVar(frac_poly).desc.content.structure.num.frac_unbound; + + try std.testing.expectEqual(false, frac_unbound.fits_in_f32); + try std.testing.expectEqual(false, frac_unbound.fits_in_dec); + } +} + +test "unify - compact vs unbound - both orders" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + // Compact should win after checking requirements + const case = try NumTestCase.init(&env, NumTypeUnbound.intLiteral(100, false), .u8); + + // Test both orders and verify compact wins + { + const unified = try case.unifyAndGetResult(); + try std.testing.expect(unified.result == .ok); + + const resolved = env.module_env.types.resolveVar(unified.a).desc.content.structure.num; + try std.testing.expect(resolved == .num_compact); + try std.testing.expectEqual(Num.Int.Precision.u8, resolved.num_compact.int); + } + + // Swapped order should also have compact win + { + const swapped_case = NumTestCase{ + .env = case.env, + .a_spec = case.b_spec, + .b_spec = case.a_spec, + }; + const unified = try swapped_case.unifyAndGetResult(); + try std.testing.expect(unified.result == .ok); + + const resolved = env.module_env.types.resolveVar(unified.a).desc.content.structure.num; + try std.testing.expect(resolved == .num_compact); + try std.testing.expectEqual(Num.Int.Precision.u8, resolved.num_compact.int); + } +} + +test "unify - compact vs compact - same type succeeds" { + const gpa = std.testing.allocator; + var env = try TestEnv.init(gpa); + defer env.deinit(); + + const int_types = [_]NumTypeBound{ .u8, .i8, .u16, .i16, .u32, .i32, .u64, .i64, .u128, .i128 }; + const frac_types = [_]NumTypeBound{ .f32, .f64, .dec }; + + // Same int types should unify + for (int_types) |t| { + const case = try NumTestCase.initBothBound(&env, t, t); + try case.expectBothOrders(NumTestCase.expectOk); + } + + // Same frac types should unify + for (frac_types) |t| { + const case = try NumTestCase.initBothBound(&env, t, t); + try case.expectBothOrders(NumTestCase.expectOk); + } + + // Different int types should NOT unify + const case_u8_i8 = try NumTestCase.initBothBound(&env, .u8, .i8); + try case_u8_i8.expectBothOrders(NumTestCase.expectProblem); + + const case_u8_u16 = try NumTestCase.initBothBound(&env, .u8, .u16); + try case_u8_u16.expectBothOrders(NumTestCase.expectProblem); + + // Int vs frac should NOT unify + const case_u8_f32 = try NumTestCase.initBothBound(&env, .u8, .f32); + try case_u8_f32.expectBothOrders(NumTestCase.expectProblem); +} diff --git a/src/check/unify.zig b/src/check/unify.zig index 3f5cc4a60a..5c10d86856 100644 --- a/src/check/unify.zig +++ b/src/check/unify.zig @@ -67,6 +67,8 @@ 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 Content = types_mod.Content; const Alias = types_mod.Alias; const NominalType = types_mod.NominalType; @@ -459,20 +461,13 @@ fn Unifier(comptime StoreTypeB: type) type { defer trace.end(); switch (vars.a.desc.content) { - .flex_var => |mb_a_ident| { - self.unifyFlex(vars, mb_a_ident, vars.b.desc.content); + .flex => |flex| { + self.unifyFlex(vars, flex, vars.b.desc.content); }, - .rigid_var => |_| { + .rigid => |_| { 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| { @@ -516,19 +511,26 @@ fn Unifier(comptime StoreTypeB: type) type { // Unify flex // /// Unify when `a` was a flex - fn unifyFlex(self: *Self, vars: *const ResolvedVarDescs, mb_a_ident: ?Ident.Idx, b_content: Content) void { + fn unifyFlex(self: *Self, vars: *const ResolvedVarDescs, a_flex: Flex, 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 }); - } + .flex => |b_flex| { + const mb_ident = blk: { + if (a_flex.name) |a_ident| { + break :blk a_ident; + } else { + break :blk b_flex.name; + } + }; + // TODO: Merge static dispatch constraints + self.merge(vars, Content{ .flex = Flex.init().withName(mb_ident) }); + }, + .rigid => { + // TODO: Merge static dispatch constraints + self.merge(vars, b_content); }, - .rigid_var => self.merge(vars, b_content), .alias => |_| self.merge(vars, b_content), .structure => self.merge(vars, b_content), .err => self.merge(vars, .err), @@ -543,8 +545,11 @@ fn Unifier(comptime StoreTypeB: type) type { defer trace.end(); switch (b_content) { - .flex_var => self.merge(vars, vars.a.desc.content), - .rigid_var => return error.TypeMismatch, + .flex => { + // TODO: Merge static dispatch constraints + self.merge(vars, vars.a.desc.content); + }, + .rigid => return error.TypeMismatch, .alias => return error.TypeMismatch, .structure => return error.TypeMismatch, .err => self.merge(vars, .err), @@ -561,10 +566,12 @@ fn Unifier(comptime StoreTypeB: type) type { const backing_var = self.types_store.getAliasBackingVar(a_alias); switch (b_content) { - .flex_var => |_| { + .flex => |_| { + // TODO: Unwrap alias? + // TODO: Merge static dispatch constraints self.merge(vars, Content{ .alias = a_alias }); }, - .rigid_var => |_| { + .rigid => |_| { try self.unifyGuarded(backing_var, vars.b.var_); }, .alias => |b_alias| { @@ -583,7 +590,21 @@ fn Unifier(comptime StoreTypeB: type) type { } }, .structure => { - try self.unifyGuarded(backing_var, vars.b.var_); + // 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; + self.types_store.setVarRedirect(vars.a.var_, fresh_alias_var) catch return Error.AllocatorError; + self.types_store.setVarRedirect(vars.b.var_, fresh_alias_var) catch return Error.AllocatorError; }, .err => self.merge(vars, .err), } @@ -637,12 +658,29 @@ fn Unifier(comptime StoreTypeB: type) type { defer trace.end(); switch (b_content) { - .flex_var => |_| { + .flex => |_| { + // TODO: Check static dispatch constraints self.merge(vars, Content{ .structure = a_flat_type }); }, - .rigid_var => return error.TypeMismatch, + .rigid => return error.TypeMismatch, .alias => |b_alias| { - try self.unifyGuarded(vars.a.var_, self.types_store.getAliasBackingVar(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; + self.types_store.setVarRedirect(vars.a.var_, fresh_alias_var) catch return Error.AllocatorError; + self.types_store.setVarRedirect(vars.b.var_, fresh_alias_var) catch return Error.AllocatorError; }, .structure => |b_flat_type| { try self.unifyFlatType(vars, a_flat_type, b_flat_type); @@ -819,49 +857,22 @@ fn Unifier(comptime StoreTypeB: type) type { } }, .record => |b_record| { - try self.unifyTwoRecords(vars, a_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| { - // 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( + try self.unifyTwoRecords( vars, - self.scratch.in_both_fields.sliceRange(partitioned.in_both), - null, - null, - a_gathered_fields.ext, + a_record.fields, + .{ .ext = a_record.ext }, + b_fields, + .unbound, ); - - // 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, } @@ -877,173 +888,22 @@ fn Unifier(comptime StoreTypeB: type) type { } }, .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( + try self.unifyTwoRecords( vars, - self.scratch.in_both_fields.sliceRange(partitioned.in_both), - null, - null, - b_gathered_fields.ext, + a_fields, + .unbound, + b_record.fields, + .{ .ext = b_record.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, + try self.unifyTwoRecords( + vars, a_fields, - ) catch return Error.AllocatorError; - const b_gathered_range = self.scratch.copyGatherFieldsFromMultiList( - &self.types_store.record_fields, + .unbound, 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, + .unbound, ); - - // 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, } @@ -1068,13 +928,6 @@ fn Unifier(comptime StoreTypeB: type) type { 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, } }, @@ -1144,335 +997,163 @@ fn Unifier(comptime StoreTypeB: type) type { a_num: Num, b_num: Num, ) Error!void { - const trace = tracy.trace(@src()); - defer trace.end(); - switch (a_num) { + // Nums // + // Num(a) + // ^^^ .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), - } } } }); + try self.unifyGuarded(a_poly, b_poly); + self.merge(vars, .{ .structure = .{ .num = .{ .num_poly = b_poly } } }); }, - .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_unbound => |b_reqs| { + try self.unifyPolyAndUnboundNums(vars, a_poly, b_reqs); }, .num_compact => |b_num_compact| { - // num_poly always contains IntRequirements - switch (b_num_compact) { - .int => |prec| { - const result = self.checkIntPrecisionRequirements(prec, a_poly.requirements); + try self.unifyPolyAndCompactNums(vars, a_poly, b_num_compact); + }, + else => return error.TypeMismatch, + } + }, + .num_unbound => |a_reqs| { + switch (b_num) { + .num_poly => |b_poly| { + try self.unifyUnboundAndPolyNums(vars, a_reqs, b_poly); + }, + .num_unbound => |b_requirements| { + self.merge(vars, .{ .structure = .{ .num = .{ + .num_unbound = .{ + .int_requirements = a_reqs.int_requirements.unify(b_requirements.int_requirements), + .frac_requirements = a_reqs.frac_requirements.unify(b_requirements.frac_requirements), + }, + } } }); + }, + .num_compact => |b_num_compact| { + try self.unifyUnboundAndCompactNums( + vars, + a_reqs, + b_num_compact, + ); + }, + else => return error.TypeMismatch, + } + }, + // Ints + // Num(Int(a)) + // ^^^^^^ + .int_poly => |a_poly_var| { + switch (b_num) { + .int_poly => |b_poly_var| { + try self.unifyGuarded(a_poly_var, b_poly_var); + self.merge(vars, vars.a.desc.content); + }, + .int_unbound => |b_reqs| { + const a_num_resolved = self.resolvePolyNum(a_poly_var, .inside_int); + switch (a_num_resolved) { + .int_resolved => |a_prec| { + const result = self.checkIntPrecisionRequirements(a_prec, b_reqs); switch (result) { .ok => {}, .negative_unsigned => return error.NegativeUnsignedInt, .too_large => return error.NumberDoesNotFit, } + self.merge(vars, vars.a.desc.content); }, - .frac => return error.TypeMismatch, + .int_flex => { + self.merge(vars, vars.b.desc.content); + }, + else => 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| { + .int_unbound => |a_reqs| { 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; + .int_poly => |b_poly_var| { + const b_num_resolved = self.resolvePolyNum(b_poly_var, .inside_int); + switch (b_num_resolved) { + .int_resolved => |b_prec| { + const result = self.checkIntPrecisionRequirements(b_prec, a_reqs); + switch (result) { + .ok => {}, + .negative_unsigned => return error.NegativeUnsignedInt, + .too_large => return error.NumberDoesNotFit, } 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, - } + .int_flex => { + self.merge(vars, vars.a.desc.content); }, - .frac => return error.TypeMismatch, + else => 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_precision => |prec| { - // Promote decimal integers to the concrete fractional precision. - // Any fractional precision can represent integer literals, so no - // additional requirement checks are needed here. - self.merge(vars, Content{ .structure = .{ .num = .{ .frac_precision = prec } } }); - }, - .frac_unbound => |b_requirements| { - // When unifying num_unbound with frac_unbound, frac wins - self.merge(vars, Content{ .structure = .{ .num = .{ .frac_unbound = b_requirements } } }); + .int_unbound => |b_reqs| { + self.merge(vars, .{ .structure = .{ .num = .{ + .int_unbound = a_reqs.unify(b_reqs), + } } }); }, else => return error.TypeMismatch, } }, - .int_unbound => |a_requirements| { + // Fracs // + // Num(Frac(a)) + // ^^^^^^^ + .frac_poly => |a_poly_var| { 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), - } } } }); + .frac_poly => |b_poly_var| { + try self.unifyGuarded(a_poly_var, b_poly_var); + self.merge(vars, vars.a.desc.content); }, - .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_unbound => |b_reqs| { + const a_num_resolved = self.resolvePolyNum(a_poly_var, .inside_frac); + switch (a_num_resolved) { + .frac_resolved => |a_prec| { + const does_fit = self.checkFracPrecisionRequirements(a_prec, b_reqs); + if (!does_fit) { + return error.NumberDoesNotFit; } + self.merge(vars, vars.a.desc.content); }, - .frac => return error.TypeMismatch, + .frac_flex => { + self.merge(vars, vars.b.desc.content); + }, + else => 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| { + .frac_unbound => |a_reqs| { 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; + .frac_poly => |b_poly_var| { + const b_num_resolved = self.resolvePolyNum(b_poly_var, .inside_frac); + switch (b_num_resolved) { + .frac_resolved => |b_prec| { + const does_fit = self.checkFracPrecisionRequirements(b_prec, a_reqs); + if (!does_fit) { + return error.NumberDoesNotFit; } + self.merge(vars, vars.b.desc.content); }, - .int => return error.TypeMismatch, + .frac_flex => { + self.merge(vars, vars.a.desc.content); + }, + else => 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 } } }); + .frac_unbound => |b_reqs| { + self.merge(vars, .{ .structure = .{ .num = .{ + .frac_unbound = a_reqs.unify(b_reqs), + } } }); }, else => return error.TypeMismatch, } }, + // Precisions // + // This Num(Int(a)), Num(Int(Signed8)), Num(Frac(...)) + // ^ ^^^^^^^ ^^^ .int_precision => |a_prec| { switch (b_num) { .int_precision => |b_prec| { @@ -1482,18 +1163,6 @@ fn Unifier(comptime StoreTypeB: type) type { 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, } }, @@ -1506,100 +1175,30 @@ fn Unifier(comptime StoreTypeB: type) type { return error.TypeMismatch; } }, - .num_unbound => { - // Fractional precision wins when unified with decimal integer literals. - self.merge(vars, vars.a.desc.content); - }, - .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, } }, + // Compacted nums // + // The whole Num(Int(Signed8)), compacted into a single variable + // ^^^^^^^^^^^^^^^^ .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); + try self.unifyCompactAndPolyNums( + vars, + a_num_compact, + b_poly, + ); }, - .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); + .num_unbound => |b_reqs| { + try self.unifyCompactAndUnboundNums( + vars, + a_num_compact, + b_reqs, + ); }, else => return error.TypeMismatch, } @@ -1607,71 +1206,411 @@ fn Unifier(comptime StoreTypeB: type) type { } } - const IntPrecisionCheckResult = enum { - ok, - negative_unsigned, - too_large, - }; + // number unification helpers // - fn checkIntPrecisionRequirements(self: *Self, prec: Num.Int.Precision, requirements: Num.IntRequirements) IntPrecisionCheckResult { - _ = self; + // Unify when a is polymorphic and b is unbound with requirements + /// Preserves rigid variables from a, or merges requirements appropriately + fn unifyPolyAndUnboundNums( + self: *Self, + vars: *const ResolvedVarDescs, + a_num_var: Var, + b_reqs: Num.NumRequirements, + ) Error!void { + const a_num_resolved = self.resolvePolyNum(a_num_var, .inside_num); + switch (a_num_resolved) { + // If the variable inside a was flex, then b wins + .num_flex => self.merge(vars, vars.b.desc.content), - // Check sign requirement - const is_signed = switch (prec) { - .i8, .i16, .i32, .i64, .i128 => true, - .u8, .u16, .u32, .u64, .u128 => false, - }; + // If the variable inside a was flex, then have it become unbound with requirements + .int_flex => { + const int_unbound = self.fresh(vars, .{ .structure = .{ + .num = .{ .int_unbound = b_reqs.int_requirements }, + } }) catch return Error.AllocatorError; + self.merge(vars, .{ .structure = .{ .num = .{ .num_poly = int_unbound } } }); + }, + .frac_flex => { + const frac_unbound = self.fresh(vars, .{ .structure = .{ + .num = .{ .frac_unbound = b_reqs.frac_requirements }, + } }) catch return Error.AllocatorError; + self.merge(vars, .{ .structure = .{ .num = .{ .num_poly = frac_unbound } } }); + }, - // 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; + // If the variable was rigid, then the rigid wins + .num_rigid => self.merge(vars, vars.a.desc.content), + .int_rigid => self.merge(vars, vars.a.desc.content), + .frac_rigid => self.merge(vars, vars.a.desc.content), + + // If the variable inside a was unbound with recs, unify the reqs + .num_unbound => |a_reqs| self.merge(vars, .{ .structure = .{ .num = .{ .num_unbound = .{ + .int_requirements = b_reqs.int_requirements.unify(a_reqs.int_requirements), + .frac_requirements = b_reqs.frac_requirements.unify(a_reqs.frac_requirements), + } } } }), + .int_unbound => |a_reqs| { + const poly_unbound = self.fresh(vars, .{ .structure = .{ + .num = .{ .int_unbound = b_reqs.int_requirements.unify(a_reqs) }, + } }) catch return Error.AllocatorError; + self.merge(vars, .{ .structure = .{ .num = .{ .num_poly = poly_unbound } } }); + }, + .frac_unbound => |a_reqs| { + const poly_unbound = self.fresh(vars, .{ .structure = .{ + .num = .{ .frac_unbound = b_reqs.frac_requirements.unify(a_reqs) }, + } }) catch return Error.AllocatorError; + self.merge(vars, .{ .structure = .{ .num = .{ .num_poly = poly_unbound } } }); + }, + + // If the variable inside an int with a precision + .int_resolved => |a_int| { + const result = self.checkIntPrecisionRequirements(a_int, b_reqs.int_requirements); + switch (result) { + .ok => {}, + .negative_unsigned => return error.NegativeUnsignedInt, + .too_large => return error.NumberDoesNotFit, + } + self.merge(vars, vars.b.desc.content); + }, + + // If the variable inside an frac with a precision or requirements + .frac_resolved => |a_frac| { + const does_fit = self.checkFracPrecisionRequirements(a_frac, b_reqs.frac_requirements); + if (!does_fit) { + return error.NumberDoesNotFit; + } + self.merge(vars, vars.b.desc.content); + }, + + // If the variable inside a wasn't a num, then this in an error + .err => |var_| { + return self.setUnifyErrAndThrow(.{ .invalid_number_type = var_ }); + }, } - - // 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; + /// Unify when a is unbound with requirements and b is polymorphic + /// Preserves rigid variables from b, or merges requirements appropriately + fn unifyUnboundAndPolyNums( + self: *Self, + vars: *const ResolvedVarDescs, + a_reqs: Num.NumRequirements, + b_num_var: Var, + ) Error!void { + const b_num_resolved = self.resolvePolyNum(b_num_var, .inside_num); + switch (b_num_resolved) { + // If the variable inside a was flex, then b wins + .num_flex => self.merge(vars, vars.a.desc.content), + + // If the variable inside a was flex, then have it become unbound with requirements + .int_flex => { + const int_unbound = self.fresh(vars, .{ .structure = .{ + .num = .{ .int_unbound = a_reqs.int_requirements }, + } }) catch return Error.AllocatorError; + self.merge(vars, .{ .structure = .{ .num = .{ .num_poly = int_unbound } } }); + }, + .frac_flex => { + const frac_unbound = self.fresh(vars, .{ .structure = .{ + .num = .{ .frac_unbound = a_reqs.frac_requirements }, + } }) catch return Error.AllocatorError; + self.merge(vars, .{ .structure = .{ .num = .{ .num_poly = frac_unbound } } }); + }, + + // If the variable was rigid, then the rigid wins + .num_rigid => self.merge(vars, vars.b.desc.content), + .int_rigid => self.merge(vars, vars.b.desc.content), + .frac_rigid => self.merge(vars, vars.b.desc.content), + + // If the variable inside a was unbound with recs, unify the reqs + .num_unbound => |b_reqs| self.merge(vars, .{ .structure = .{ .num = .{ .num_unbound = .{ + .int_requirements = a_reqs.int_requirements.unify(b_reqs.int_requirements), + .frac_requirements = a_reqs.frac_requirements.unify(b_reqs.frac_requirements), + } } } }), + .int_unbound => |b_reqs| { + const poly_unbound = self.fresh(vars, .{ .structure = .{ + .num = .{ .int_unbound = a_reqs.int_requirements.unify(b_reqs) }, + } }) catch return Error.AllocatorError; + self.merge(vars, .{ .structure = .{ .num = .{ .num_poly = poly_unbound } } }); + }, + .frac_unbound => |b_reqs| { + const poly_unbound = self.fresh(vars, .{ .structure = .{ + .num = .{ .frac_unbound = a_reqs.frac_requirements.unify(b_reqs) }, + } }) catch return Error.AllocatorError; + self.merge(vars, .{ .structure = .{ .num = .{ .num_poly = poly_unbound } } }); + }, + + // If the variable inside an int with a precision + .int_resolved => |b_int| { + const result = self.checkIntPrecisionRequirements(b_int, a_reqs.int_requirements); + switch (result) { + .ok => {}, + .negative_unsigned => return error.NegativeUnsignedInt, + .too_large => return error.NumberDoesNotFit, + } + self.merge(vars, vars.a.desc.content); + }, + + // If the variable inside an frac with a precision or requirements + .frac_resolved => |b_frac| { + const does_fit = self.checkFracPrecisionRequirements(b_frac, a_reqs.frac_requirements); + if (!does_fit) { + return error.NumberDoesNotFit; + } + self.merge(vars, vars.a.desc.content); + }, + + // If the variable inside a wasn't a num, then this in an error + .err => |var_| { + return self.setUnifyErrAndThrow(.{ .invalid_number_type = var_ }); + }, + } } - fn fracPrecisionSatisfiesRequirements(self: *Self, prec: Num.Frac.Precision, requirements: Num.FracRequirements) bool { - _ = self; + /// Unify when a is compact and b is polymorphic + /// Since a is compact, we must merge with it (unless b is rigid, which errors) + fn unifyCompactAndPolyNums( + self: *Self, + vars: *const ResolvedVarDescs, + a_num: NumCompact, + b_num_var: Var, + ) Error!void { + const b_num_resolved = self.resolvePolyNum(b_num_var, .inside_num); + switch (a_num) { + .int => |a_int| { + switch (b_num_resolved) { + // If the variable inside a was flex, then b wins + .num_flex => self.merge(vars, vars.a.desc.content), + .int_flex => self.merge(vars, vars.a.desc.content), - switch (prec) { - .f32 => return requirements.fits_in_f32, - .f64 => return true, // F64 can always hold values - .dec => return requirements.fits_in_dec, + // If the var inside was a num with requirements + .num_unbound => |b_reqs| { + const result = self.checkIntPrecisionRequirements(a_int, b_reqs.int_requirements); + switch (result) { + .ok => {}, + .negative_unsigned => return error.NegativeUnsignedInt, + .too_large => return error.NumberDoesNotFit, + } + self.merge(vars, vars.a.desc.content); + }, + + // If the variable inside an int with a precision or requirements + .int_resolved => |b_int| if (@intFromEnum(a_int) == @intFromEnum(b_int)) { + self.merge(vars, vars.a.desc.content); + } else { + return error.TypeMismatch; + }, + .int_unbound => |b_reqs| { + const result = self.checkIntPrecisionRequirements(a_int, b_reqs); + switch (result) { + .ok => {}, + .negative_unsigned => return error.NegativeUnsignedInt, + .too_large => return error.NumberDoesNotFit, + } + self.merge(vars, vars.a.desc.content); + }, + + // If the variable inside b was a frac, error + .frac_flex => return error.TypeMismatch, + .frac_resolved => return error.TypeMismatch, + .frac_unbound => return error.TypeMismatch, + + // If the variable was rigid, then error + .num_rigid => return error.TypeMismatch, + .int_rigid => return error.TypeMismatch, + .frac_rigid => return error.TypeMismatch, + + // If the variable inside a wasn't a num, then this in an error + .err => |var_| { + return self.setUnifyErrAndThrow(.{ .invalid_number_type = var_ }); + }, + } + }, + .frac => |a_frac| { + switch (b_num_resolved) { + // If the variable inside a was flex, then b wins + .num_flex => self.merge(vars, vars.a.desc.content), + .frac_flex => self.merge(vars, vars.a.desc.content), + + // If the var inside was a num with requirements + .num_unbound => |b_reqs| { + const does_fit = self.checkFracPrecisionRequirements(a_frac, b_reqs.frac_requirements); + if (!does_fit) { + return error.NumberDoesNotFit; + } + self.merge(vars, vars.a.desc.content); + }, + + // If the variable inside an int with a precision or requirements + .frac_resolved => |b_frac| if (@intFromEnum(a_frac) == @intFromEnum(b_frac)) { + self.merge(vars, vars.a.desc.content); + } else { + return error.TypeMismatch; + }, + .frac_unbound => |b_reqs| { + const does_fit = self.checkFracPrecisionRequirements(a_frac, b_reqs); + if (!does_fit) { + return error.NumberDoesNotFit; + } + self.merge(vars, vars.a.desc.content); + }, + + // If the variable inside b was an int, error + .int_flex => return error.TypeMismatch, + .int_resolved => return error.TypeMismatch, + .int_unbound => return error.TypeMismatch, + + // If the variable was rigid, then error + .num_rigid => return error.TypeMismatch, + .int_rigid => return error.TypeMismatch, + .frac_rigid => return error.TypeMismatch, + + // If the variable inside a wasn't a num, then this in an error + .err => |var_| { + return self.setUnifyErrAndThrow(.{ .invalid_number_type = var_ }); + }, + } + }, + } + } + + /// Unify when a is polymorphic and b is compact + /// Since b is compact, we must merge with it (unless a is rigid, which errors) + fn unifyPolyAndCompactNums( + self: *Self, + vars: *const ResolvedVarDescs, + a_num_var: Var, + b_num: NumCompact, + ) Error!void { + const a_num_resolved = self.resolvePolyNum(a_num_var, .inside_num); + switch (a_num_resolved) { + // If the variable inside a was flex, then b wins + .num_flex => self.merge(vars, vars.b.desc.content), + .int_flex => self.merge(vars, vars.b.desc.content), + .frac_flex => self.merge(vars, vars.b.desc.content), + + // If the variable was rigid, then error + .num_rigid => return error.TypeMismatch, + .int_rigid => return error.TypeMismatch, + .frac_rigid => return error.TypeMismatch, + + // If the var inside was a num with requirements + .num_unbound => |a_reqs| switch (b_num) { + .int => |b_int| { + const result = self.checkIntPrecisionRequirements(b_int, a_reqs.int_requirements); + switch (result) { + .ok => {}, + .negative_unsigned => return error.NegativeUnsignedInt, + .too_large => return error.NumberDoesNotFit, + } + self.merge(vars, vars.b.desc.content); + }, + .frac => |b_frac| { + const does_fit = self.checkFracPrecisionRequirements(b_frac, a_reqs.frac_requirements); + if (!does_fit) { + return error.NumberDoesNotFit; + } + self.merge(vars, vars.b.desc.content); + }, + }, + + // If the variable inside an int with a precision or requirements + .int_resolved => |a_int| switch (b_num) { + .int => |b_int| if (@intFromEnum(a_int) == @intFromEnum(b_int)) { + self.merge(vars, vars.b.desc.content); + } else { + return error.TypeMismatch; + }, + .frac => return error.TypeMismatch, + }, + .int_unbound => |a_reqs| switch (b_num) { + .int => |b_int| { + const result = self.checkIntPrecisionRequirements(b_int, a_reqs); + switch (result) { + .ok => {}, + .negative_unsigned => return error.NegativeUnsignedInt, + .too_large => return error.NumberDoesNotFit, + } + self.merge(vars, vars.b.desc.content); + }, + .frac => return error.TypeMismatch, + }, + + // If the variable inside an frac with a precision or requirements + .frac_resolved => |a_frac| switch (b_num) { + .frac => |b_frac| if (@intFromEnum(a_frac) == @intFromEnum(b_frac)) { + self.merge(vars, vars.b.desc.content); + } else { + return error.TypeMismatch; + }, + .int => return error.TypeMismatch, + }, + .frac_unbound => |a_reqs| switch (b_num) { + .frac => |b_frac| { + const does_fit = self.checkFracPrecisionRequirements(b_frac, a_reqs); + if (!does_fit) { + return error.NumberDoesNotFit; + } + self.merge(vars, vars.b.desc.content); + }, + .int => return error.TypeMismatch, + }, + + // If the variable inside a wasn't a num, then this in an error + .err => |var_| { + return self.setUnifyErrAndThrow(.{ .invalid_number_type = var_ }); + }, + } + } + + /// Unify when a is compact and b is unbound with requirements + /// Since a is compact, we must merge with it after checking requirements + fn unifyCompactAndUnboundNums( + self: *Self, + vars: *const ResolvedVarDescs, + a_num: NumCompact, + b_reqs: Num.NumRequirements, + ) Error!void { + switch (a_num) { + .int => |a_int| { + const result = self.checkIntPrecisionRequirements(a_int, b_reqs.int_requirements); + switch (result) { + .ok => {}, + .negative_unsigned => return error.NegativeUnsignedInt, + .too_large => return error.NumberDoesNotFit, + } + self.merge(vars, vars.a.desc.content); + }, + .frac => |a_frac| { + const does_fit = self.checkFracPrecisionRequirements(a_frac, b_reqs.frac_requirements); + if (!does_fit) { + return error.NumberDoesNotFit; + } + self.merge(vars, vars.a.desc.content); + }, + } + } + + /// Unify when a is unbound with requirements and b is compact + /// Since b is compact, we must merge with it after checking requirements + fn unifyUnboundAndCompactNums( + self: *Self, + vars: *const ResolvedVarDescs, + a_reqs: Num.NumRequirements, + b_num: NumCompact, + ) Error!void { + switch (b_num) { + .int => |b_int| { + const result = self.checkIntPrecisionRequirements(b_int, a_reqs.int_requirements); + switch (result) { + .ok => {}, + .negative_unsigned => return error.NegativeUnsignedInt, + .too_large => return error.NumberDoesNotFit, + } + self.merge(vars, vars.b.desc.content); + }, + .frac => |b_frac| { + const does_fit = self.checkFracPrecisionRequirements(b_frac, a_reqs.frac_requirements); + if (!does_fit) { + return error.NumberDoesNotFit; + } + self.merge(vars, vars.b.desc.content); + }, } } @@ -1708,14 +1647,97 @@ fn Unifier(comptime StoreTypeB: type) type { } } + // number requirement helpers // + + /// The result of checking if an imprecision is compatible with a set of requirements. + const IntPrecisionCheckResult = enum { + ok, + negative_unsigned, + too_large, + }; + + /// Checks whether a chosen integer precision can satisfy unified IntRequirements + /// under two’s-complement semantics, using only sign_needed, bits_needed, and is_minimum_signed. + /// + /// Rules: + /// - Unsigned N-bit: accept if bits_needed ≤ N. + /// - Signed N-bit: + /// * Positive: accept if bits_needed ≤ N−1. + /// * Negative: accept if (bits_needed ≤ N−1) + /// OR (bits_needed == N AND is_minimum_signed), + /// where the latter covers the single boundary value −2^(N−1). + /// + /// TODO: Review, claude generated + fn checkIntPrecisionRequirements(self: *Self, prec: Num.Int.Precision, reqs: Num.IntRequirements) IntPrecisionCheckResult { + _ = self; + + const is_signed = switch (prec) { + .i8, .i16, .i32, .i64, .i128 => true, + .u8, .u16, .u32, .u64, .u128 => false, + }; + + if (reqs.sign_needed and !is_signed) { + return .negative_unsigned; + } + + const n: u8 = switch (prec) { + .i8, .u8 => 8, + .i16, .u16 => 16, + .i32, .u32 => 32, + .i64, .u64 => 64, + .i128, .u128 => 128, + }; + + const k: u8 = reqs.bits_needed; + + if (!is_signed) { + return if (k <= n) .ok else .too_large; + } + + if (reqs.sign_needed) { + const fits_regular_neg = (k <= n - 1); + const fits_boundary_neg = (k == n) and reqs.is_minimum_signed; // only allow −2^(N−1) + return if (fits_regular_neg or fits_boundary_neg) .ok else .too_large; + } else { + return if (k <= n - 1) .ok else .too_large; + } + } + + /// Checks if the frac precision satisfies the requirements + fn checkFracPrecisionRequirements(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, + } + } + + // polymorphic num helpers // + /// The result of attempting to resolve a polymorphic number const ResolvedNum = union(enum) { - flex_resolved, + num_flex, + num_rigid: Var, + num_unbound: Num.NumRequirements, + int_flex, + int_rigid: Var, + int_unbound: Num.IntRequirements, int_resolved: Num.Int.Precision, + frac_flex, + frac_rigid: Var, + frac_unbound: Num.FracRequirements, frac_resolved: Num.Frac.Precision, err: Var, }; + const ResolvePolyNumCtx = enum { + inside_num, + inside_int, + inside_frac, + }; + /// Attempts to resolve a polymorphic number variable to a concrete precision. /// /// This function recursively follows the structure of a number type, @@ -1726,7 +1748,7 @@ fn Unifier(comptime StoreTypeB: type) type { /// 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`, + /// If resolution reaches a `.flex`, it returns `.flex_resolved`, /// indicating the number is still unspecialized. /// /// If the chain ends in an invalid structure (e.g. `Num(Str)`), @@ -1736,26 +1758,60 @@ fn Unifier(comptime StoreTypeB: type) type { /// 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 { + fn resolvePolyNum(self: *Self, initial_num_var: Var, initial_ctx: ResolvePolyNumCtx) ResolvedNum { var num_var = initial_num_var; + var seen_int = initial_ctx == .inside_int; + var seen_frac = initial_ctx == .inside_frac; while (true) { const resolved = self.types_store.resolveVar(num_var); switch (resolved.desc.content) { - .flex_var => return .flex_resolved, + .flex => { + if (seen_int and seen_frac) { + return .{ .err = num_var }; + } else if (seen_int) { + return .int_flex; + } else if (seen_frac) { + return .frac_flex; + } else { + return .num_flex; + } + }, + .rigid => { + if (seen_int and seen_frac) { + return .{ .err = num_var }; + } else if (seen_int) { + return .{ .int_rigid = num_var }; + } else if (seen_frac) { + return .{ .frac_rigid = num_var }; + } else { + return .{ .num_rigid = num_var }; + } + }, + .alias => |alias| { + num_var = self.types_store.getAliasBackingVar(alias); + }, .structure => |flat_type| { switch (flat_type) { .num => |num| switch (num) { - .num_poly => |requirements| { - num_var = requirements.var_; + .num_poly => |var_| { + num_var = var_; }, - .int_poly => |requirements| { - num_var = requirements.var_; + .num_unbound => |reqs| { + return .{ .num_unbound = reqs }; }, - .frac_poly => |requirements| { - num_var = requirements.var_; + .int_poly => |var_| { + seen_int = true; + num_var = var_; + }, + .int_unbound => |reqs| { + return .{ .int_unbound = reqs }; + }, + .frac_poly => |var_| { + seen_frac = true; + num_var = var_; + }, + .frac_unbound => |reqs| { + return .{ .frac_unbound = reqs }; }, .int_precision => |prec| { return .{ .int_resolved = prec }; @@ -1925,16 +1981,18 @@ fn Unifier(comptime StoreTypeB: type) type { fn unifyTwoRecords( self: *Self, vars: *const ResolvedVarDescs, - a_record: Record, - b_record: Record, + 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_record); - const b_gathered_fields = try self.gatherRecordFields(b_record); + 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( @@ -1957,11 +2015,24 @@ fn Unifier(comptime StoreTypeB: type) type { 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_fields.ext, b_gathered_fields.ext); + try self.unifyGuarded(a_gathered_ext, b_gathered_ext); // Unify shared fields // This copies fields from scratch into type_store @@ -1970,7 +2041,7 @@ fn Unifier(comptime StoreTypeB: type) type { self.scratch.in_both_fields.sliceRange(partitioned.in_both), null, null, - a_gathered_fields.ext, + a_gathered_ext, ); }, .a_extends_b => { @@ -1981,11 +2052,11 @@ fn Unifier(comptime StoreTypeB: type) type { ) 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, + .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_fields.ext); + try self.unifyGuarded(only_in_a_var, b_gathered_ext); // Unify shared fields // This copies fields from scratch into type_store @@ -2005,11 +2076,11 @@ fn Unifier(comptime StoreTypeB: type) type { ) 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, + .ext = b_gathered_ext, } } }) catch return Error.AllocatorError; // Unify the sub record with a's ext - try self.unifyGuarded(a_gathered_fields.ext, only_in_b_var); + try self.unifyGuarded(a_gathered_ext, only_in_b_var); // Unify shared fields // This copies fields from scratch into type_store @@ -2029,7 +2100,7 @@ fn Unifier(comptime StoreTypeB: type) type { ) 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, + .ext = a_gathered_ext, } } }) catch return Error.AllocatorError; // Create a new variable of a record with only b's uniq fields @@ -2039,15 +2110,15 @@ fn Unifier(comptime StoreTypeB: type) type { ) 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, + .ext = b_gathered_ext, } } }) catch return Error.AllocatorError; // Create a new ext var - const new_ext_var = self.fresh(vars, .{ .flex_var = null }) catch return Error.AllocatorError; + 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_fields.ext, only_in_b_var); - try self.unifyGuarded(only_in_a_var, b_gathered_fields.ext); + 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 @@ -2064,7 +2135,9 @@ fn Unifier(comptime StoreTypeB: type) type { const FieldsExtension = enum { exactly_the_same, a_extends_b, b_extends_a, both_extend }; - const GatheredFields = struct { ext: Var, range: RecordFieldSafeList.Range }; + 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: @@ -2076,61 +2149,60 @@ fn Unifier(comptime StoreTypeB: type) type { /// * 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 { + 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, + record_fields, ) catch return Error.AllocatorError; // then recursiv - var ext_var = record.ext; + var ext = record_ext; while (true) { - switch (self.types_store.resolveVar(ext_var).desc.content) { - .flex_var => { - return .{ .ext = ext_var, .range = range }; + switch (ext) { + .unbound => { + return .{ .ext = ext, .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; + .ext => |ext_var| { + switch (self.types_store.resolveVar(ext_var).desc.content) { + .flex => { + return .{ .ext = .{ .ext = ext_var }, .range = range }; }, - .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 }; + .rigid => { + return .{ .ext = .{ .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; + .alias => |alias| { + ext = .{ .ext = self.types_store.getAliasBackingVar(alias) }; }, - .empty_record => { - return .{ .ext = ext_var, .range = range }; + .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 = .{ .ext = 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, .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 }), } }, - else => try self.setUnifyErrAndThrow(.{ .invalid_record_ext = ext_var }), } } } @@ -2460,7 +2532,7 @@ fn Unifier(comptime StoreTypeB: type) type { } } }) catch return Error.AllocatorError; // Create a new ext var - const new_ext_var = self.fresh(vars, .{ .flex_var = null }) catch return Error.AllocatorError; + const new_ext_var = self.fresh(vars, .{ .flex = Flex.init() }) catch return Error.AllocatorError; // Unify the sub tag_unions with exts try self.unifyGuarded(a_gathered_tags.ext, only_in_b_var); @@ -2505,10 +2577,10 @@ fn Unifier(comptime StoreTypeB: type) type { var ext_var = tag_union.ext; while (true) { switch (self.types_store.resolveVar(ext_var).desc.content) { - .flex_var => { + .flex => { return .{ .ext = ext_var, .range = range }; }, - .rigid_var => { + .rigid => { return .{ .ext = ext_var, .range = range }; }, .alias => |alias| { @@ -2666,7 +2738,7 @@ fn Unifier(comptime StoreTypeB: type) type { } /// Set error data in scratch & throw - fn setUnifyErrAndThrow(self: *Self, err: UnifyErrCtx) Error!void { + inline fn setUnifyErrAndThrow(self: *Self, err: UnifyErrCtx) Error!void { self.scratch.setUnifyErr(err); return error.UnifyErr; } @@ -2846,11 +2918,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); } @@ -2858,3047 +2932,3 @@ pub const Scratch = struct { self.err = err; } }; - -// tests // - -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, - } - } - - /// 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_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/bench.zig b/src/cli/bench.zig index 3a9d84360d..a439d7e4be 100644 --- a/src/cli/bench.zig +++ b/src/cli/bench.zig @@ -78,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(); @@ -102,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; diff --git a/src/cli/cli_args.zig b/src/cli/cli_args.zig index 69db6b015f..9922850331 100644 --- a/src/cli/cli_args.zig +++ b/src/cli/cli_args.zig @@ -11,7 +11,7 @@ pub const CliArgs = union(enum) { run: RunArgs, check: CheckArgs, build: BuildArgs, - format: FormatArgs, + fmt: FormatArgs, test_cmd: TestArgs, bundle: BundleArgs, unbundle: UnbundleArgs, @@ -24,7 +24,7 @@ pub const CliArgs = union(enum) { pub fn deinit(self: CliArgs, gpa: mem.Allocator) void { switch (self) { - .format => |fmt| gpa.free(fmt.paths), + .fmt => |fmt| gpa.free(fmt.paths), .run => |run| gpa.free(run.app_args), .bundle => |bundle| gpa.free(bundle.paths), .unbundle => |unbundle| gpa.free(unbundle.paths), @@ -117,9 +117,13 @@ 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 }; /// Parse a list of arguments. @@ -130,7 +134,7 @@ pub fn parse(gpa: mem.Allocator, args: []const []const u8) !CliArgs { 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], "fmt")) return try parseFormat(gpa, 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..]); @@ -154,7 +158,7 @@ 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 @@ -429,7 +433,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] @@ -454,7 +458,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 { @@ -565,9 +569,14 @@ fn parseLicenses(args: []const []const u8) CliArgs { } 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 = @@ -576,26 +585,38 @@ 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 = CliProblem{ .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" } } }; - } + } 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 } } }; @@ -604,7 +625,7 @@ fn parseDocs(args: []const []const u8) CliArgs { } } - 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 { @@ -800,61 +821,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]); } } @@ -1018,20 +1039,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" }); @@ -1048,6 +1083,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" { diff --git a/src/cli/main.zig b/src/cli/main.zig index c1de2a2198..0bb89cd754 100644 --- a/src/cli/main.zig +++ b/src/cli/main.zig @@ -449,7 +449,7 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void { .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), + .fmt => |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}", .{build_options.compiler_version}), @@ -1368,6 +1368,10 @@ pub fn setupSharedMemoryWithModuleEnv(gpa: std.mem.Allocator, roc_file_path: []c const basename = std.fs.path.basename(roc_file_path); const module_name = try shm_allocator.dupe(u8, basename); + // Create arena allocator for scratch memory + var arena = std.heap.ArenaAllocator.init(shm_allocator); + defer arena.deinit(); + var env = try ModuleEnv.init(shm_allocator, source); env.common.source = source; env.module_name = module_name; @@ -1381,12 +1385,18 @@ pub fn setupSharedMemoryWithModuleEnv(gpa: std.mem.Allocator, roc_file_path: []c // Initialize CIR fields in ModuleEnv try env.initCIRFields(shm_allocator, module_name); + const common_idents: Check.CommonIdents = .{ + .module_name = try env.insertIdent(base.Ident.for_text("test")), + .list = try env.insertIdent(base.Ident.for_text("List")), + .box = try env.insertIdent(base.Ident.for_text("Box")), + }; // Create canonicalizer - var canonicalizer = try Can.init(&env, &parse_ast, null); + var canonicalizer = try Can.init(&env, &parse_ast, null, .{}); // Canonicalize the entire module try canonicalizer.canonicalizeFile(); + try canonicalizer.validateForExecution(); // Validation check - ensure exports were populated during canonicalization if (env.exports.span.len == 0) { @@ -1413,8 +1423,8 @@ pub fn setupSharedMemoryWithModuleEnv(gpa: std.mem.Allocator, roc_file_path: []c } // Type check the module - var checker = try Check.init(shm_allocator, &env.types, &env, &.{}, &env.store.regions); - try checker.checkDefs(); + var checker = try Check.init(shm_allocator, &env.types, &env, &.{}, &env.store.regions, common_idents); + try checker.checkFile(); // Copy the ModuleEnv to the allocated space env_ptr.* = env; @@ -1754,6 +1764,10 @@ fn extractEntrypointsFromPlatform(gpa: std.mem.Allocator, roc_file_path: []const const module_name = try gpa.dupe(u8, basename); defer gpa.free(module_name); + // Create arena allocator for scratch memory + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + // Create ModuleEnv var env = ModuleEnv.init(gpa, source) catch return error.ParseFailed; defer env.deinit(); @@ -2418,6 +2432,10 @@ fn rocTest(gpa: Allocator, args: cli_args.TestArgs) !void { const module_name = try gpa.dupe(u8, basename); defer gpa.free(module_name); + // Create arena allocator for scratch memory + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + // Create ModuleEnv var env = ModuleEnv.init(gpa, source) catch |err| { try stderr.print("Failed to initialize module environment: {}", .{err}); @@ -2429,6 +2447,12 @@ fn rocTest(gpa: Allocator, args: cli_args.TestArgs) !void { env.module_name = module_name; try env.common.calcLineStarts(gpa); + const module_common_idents: Check.CommonIdents = .{ + .module_name = try env.insertIdent(base.Ident.for_text(module_name)), + .list = try env.insertIdent(base.Ident.for_text("List")), + .box = try env.insertIdent(base.Ident.for_text("Box")), + }; + // Parse the source code as a full module var parse_ast = parse.parse(&env.common, gpa) catch |err| { try stderr.print("Failed to parse file: {}", .{err}); @@ -2443,7 +2467,7 @@ fn rocTest(gpa: Allocator, args: cli_args.TestArgs) !void { try env.initCIRFields(gpa, module_name); // Create canonicalizer - var canonicalizer = Can.init(&env, &parse_ast, null) catch |err| { + var canonicalizer = Can.init(&env, &parse_ast, null, .{}) catch |err| { try stderr.print("Failed to initialize canonicalizer: {}", .{err}); std.process.exit(1); }; @@ -2455,14 +2479,20 @@ fn rocTest(gpa: Allocator, args: cli_args.TestArgs) !void { std.process.exit(1); }; + // Validate for checking mode + canonicalizer.validateForChecking() catch |err| { + try stderr.print("Failed to validate module: {}", .{err}); + std.process.exit(1); + }; + // Type check the module - var checker = Check.init(gpa, &env.types, &env, &.{}, &env.store.regions) catch |err| { + var checker = Check.init(gpa, &env.types, &env, &.{}, &env.store.regions, module_common_idents) catch |err| { try stderr.print("Failed to initialize type checker: {}", .{err}); std.process.exit(1); }; defer checker.deinit(); - checker.checkDefs() catch |err| { + checker.checkFile() catch |err| { try stderr.print("Type checking failed: {}", .{err}); std.process.exit(1); }; @@ -2508,11 +2538,30 @@ fn rocTest(gpa: Allocator, args: cli_args.TestArgs) !void { if (test_result.passed) { 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 stdout.print("\x1b[31mFAIL\x1b[0m: {s}:{} - {s}\n", .{ args.path, region_info.start_line_idx + 1, msg }); - } else { - try stdout.print("\x1b[31mFAIL\x1b[0m: {s}:{}\n", .{ args.path, region_info.start_line_idx + 1 }); - } + // 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.any(), 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 }); } } } @@ -2677,6 +2726,118 @@ const BuildAppError = std.mem.Allocator.Error || std.fs.File.OpenError || std.fs CurrentWorkingDirectoryUnlinked, }; +/// 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( + gpa: Allocator, + filepath: []const u8, + collect_timing: bool, + cache_config: CacheConfig, +) 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); + build_env.compiler_version = build_options.compiler_version; + // 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()); + build_env.setCacheManager(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) + build_env.build(filepath) catch |err| { + 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 |err| { + std.debug.print("Error processing module: {}\n", .{err}); + break; + }; + } + } + } + + // 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 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), + }; + } + + // Free the original drained reports + // Note: abs_path is owned by BuildEnv, reports are moved to our array + 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( gpa: Allocator, @@ -2862,10 +3023,760 @@ fn printTimingBreakdown(writer: anytype, timing: ?CheckTimingInfo) void { } } +/// Start an HTTP server to serve the generated documentation +fn serveDocumentation(gpa: Allocator, docs_dir: []const u8) !void { + const stdout = std.io.getStdOut().writer(); + + 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(gpa, connection, docs_dir) catch |err| { + std.debug.print("Error handling connection: {}\n", .{err}); + }; + } +} + +/// Handle a single HTTP connection +fn handleConnection(gpa: Allocator, connection: std.net.Server.Connection, docs_dir: []const u8) !void { + defer connection.stream.close(); + + var buffer: [4096]u8 = undefined; + const bytes_read = try connection.stream.read(&buffer); + + 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(gpa, docs_dir, path); + defer gpa.free(file_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(gpa, 10 * 1024 * 1024); // 10MB max + defer 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 +fn resolveFilePath(gpa: Allocator, 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(gpa, "{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(gpa, "{s}/{s}", .{ docs_dir, clean_path }); + } else { + // No extension, serve index.html from that directory + return try std.fmt.allocPrint(gpa, "{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(gpa: Allocator, args: cli_args.DocsArgs) !void { - _ = gpa; - _ = args; - fatal("docs not implemented", .{}); + 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(); + + 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( + gpa, + args.path, + args.time, + cache_config, + ) catch |err| { + handleProcessFileError(err, stderr, args.path); + }; + + // Clean up when we're done - this includes the BuildEnv and all module envs + defer result_with_env.deinit(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 {}; + std.process.exit(1); + } + } 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_writer, 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}", .{report.title}) catch {}; + }; + + if (report.severity == .fatal or report.severity == .runtime_error) { + has_errors = true; + } + } + } + + 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}.", .{args.path}) catch {}; + + if (check_result.error_count > 0) { + std.process.exit(1); + } + } + } + + // Print timing breakdown if requested + if (args.time) { + printTimingBreakdown(stdout, if (builtin.target.cpu.arch == .wasm32) null else check_result.timing); + } + + // Generate documentation for all packages and modules + try generateDocs(gpa, &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(gpa, args.output); + } +} + +/// 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) + + fn deinit(self: AssociatedItem, gpa: Allocator) void { + gpa.free(self.name); + for (self.children) |child| { + child.deinit(gpa); + } + gpa.free(self.children); + } +}; + +/// 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.writeByteNTimes(' ', indent_level * 2); + try writer.writeAll("
        \n"); + + for (items) |item| { + // Write
      • with item name + try writer.writeByteNTimes(' ', (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.writeByteNTimes(' ', (indent_level + 1) * 2); + try writer.writeAll("
      • \n"); + } + + // Write closing
      + try writer.writeByteNTimes(' ', indent_level * 2); + try writer.writeAll("
    \n"); +} + +/// Generate HTML index file for a package or app +pub fn generatePackageIndex( + gpa: Allocator, + 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(gpa, &[_][]const u8{ output_path, "index.html" }); + defer gpa.free(index_path); + + const file = try std.fs.cwd().createFile(index_path, .{}); + defer file.close(); + + const writer = file.writer(); + + // 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"); +} + +/// Generate HTML index file for a module +pub fn generateModuleIndex( + gpa: Allocator, + 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(gpa, &[_][]const u8{ output_path, "index.html" }); + defer gpa.free(index_path); + + const file = try std.fs.cwd().createFile(index_path, .{}); + defer file.close(); + + const writer = file.writer(); + + // 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"); +} + +/// Extract associated items from a record expression (recursively) +fn extractRecordAssociatedItems( + gpa: Allocator, + module_env: *const ModuleEnv, + record_fields: can.CIR.RecordField.Span, +) ![]AssociatedItem { + var items = std.array_list.Managed(AssociatedItem).init(gpa); + errdefer { + for (items.items) |item| { + item.deinit(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 gpa.dupe(u8, module_env.getIdentText(field.name)); + errdefer 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(gpa, module_env, rec.fields), + else => try gpa.alloc(AssociatedItem, 0), + }; + }, + else => try 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( + gpa: Allocator, + module_env: *const ModuleEnv, +) ![]AssociatedItem { + var items = std.array_list.Managed(AssociatedItem).init(gpa); + errdefer { + for (items.items) |item| { + item.deinit(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 gpa.dupe(u8, module_env.getIdentText(name_ident_opt)); + errdefer 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(gpa, module_env, record.fields), + else => try gpa.alloc(AssociatedItem, 0), + }; + }, + else => try gpa.alloc(AssociatedItem, 0), + }; + }, + else => try 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( + gpa: Allocator, + 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(gpa, build_env, module_path, base_output_dir); + } else { + // For packages, just generate package dependency docs + try generatePackageDocs(gpa, build_env, module_path, base_output_dir, ""); + } +} + +/// Generate docs for an app module +fn generateAppDocs( + gpa: Allocator, + 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(gpa); + defer { + var it = modules_map.iterator(); + while (it.next()) |entry| { + entry.value_ptr.deinit(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 gpa.dupe(u8, ext_import); + const link_path = try std.fmt.allocPrint(gpa, "{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; + } else { + // Free the duplicates + gpa.free(full_name); + gpa.free(link_path); + } + + // Generate index.html for this module + const module_output_dir = try std.fs.path.join(gpa, &[_][]const u8{ base_output_dir, pkg_shorthand, module_name }); + defer gpa.free(module_output_dir); + generateModuleIndex(gpa, 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 gpa.dupe(u8, module_name); + const link_path = try 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(gpa, mod_env) + else + try 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 + gpa.free(full_name); + gpa.free(link_path); + for (associated_items) |item| { + item.deinit(gpa); + } + gpa.free(associated_items); + } + + // Generate index.html for this local module + const module_output_dir = try std.fs.path.join(gpa, &[_][]const u8{ base_output_dir, module_name }); + defer gpa.free(module_output_dir); + generateModuleIndex(gpa, 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.array_list.Managed(ModuleInfo).init(gpa); + defer modules_list.deinit(); + var map_iter = modules_map.iterator(); + while (map_iter.next()) |entry| { + try modules_list.append(entry.value_ptr.*); + } + + // Collect package shorthands + var shorthands_list = std.array_list.Managed([]const u8).init(gpa); + defer { + for (shorthands_list.items) |item| gpa.free(item); + shorthands_list.deinit(); + } + + var shorthand_iter = first_pkg.shorthands.iterator(); + while (shorthand_iter.next()) |sh_entry| { + const shorthand = try gpa.dupe(u8, sh_entry.key_ptr.*); + try shorthands_list.append(shorthand); + } + + // Generate root index.html + try generatePackageIndex(gpa, 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(gpa, 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( + gpa: Allocator, + 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 gpa.dupe(u8, base_output_dir) + else + try std.fs.path.join(gpa, &[_][]const u8{ base_output_dir, relative_path }); + defer gpa.free(output_dir); + + var shorthands_list = std.array_list.Managed([]const u8).init(gpa); + defer { + for (shorthands_list.items) |item| 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 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 gpa.dupe(u8, shorthand) + else + try std.fs.path.join(gpa, &[_][]const u8{ relative_path, shorthand }); + defer gpa.free(dep_relative_path); + + const dep_ref = sh_entry.value_ptr.*; + generatePackageDocs(gpa, 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(gpa); + defer { + for (module_infos.items) |mod| mod.deinit(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(gpa, mod_env); + const mod_name = try gpa.dupe(u8, module_state.name); + + try module_infos.append(.{ + .name = mod_name, + .link_path = try gpa.dupe(u8, ""), + .associated_items = associated_items, + }); + } + } + } + + generatePackageIndex(gpa, 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 }); + }; } /// Log a fatal error and exit the process with a non-zero code. diff --git a/src/cli/test_docs.zig b/src/cli/test_docs.zig new file mode 100644 index 0000000000..8492031131 --- /dev/null +++ b/src/cli/test_docs.zig @@ -0,0 +1,351 @@ +//! 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" + \\ + ); + + // Create output directory path + const output_dir = try std.fs.path.join(gpa, &[_][]const u8{ tmp_path, "generated-docs" }); + defer gpa.free(output_dir); + + const root_path = try std.fs.path.join(gpa, &[_][]const u8{ tmp_path, "root.roc" }); + defer gpa.free(root_path); + + // 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; + + _ = root_path; + _ = output_dir; +} + +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, "